如何在ASP.NET Core Webapi控制器中读取请求正文?


104

我试图读取OnActionExecuting方法中的请求正文,但我总是喜欢null该正文。

var request = context.HttpContext.Request;
var stream = new StreamReader(request.Body);
var body = stream.ReadToEnd();

我试图将流位置显式设置为0,但这也没有用。因为这是ASP.NET CORE,所以我认为情况几乎没有什么不同。我可以在这里看到所有有关旧webapi版本的示例。
还有其他方法吗?


4
请注意,如果在请求管道期间之前已经读取了请求正文,那么当您尝试第二次读取它时,它为空
Fabio


@Fabio感谢您提供信息,我们可以设置位置并再次读取吗?
Kasun Koswattha

@KasunKoswattha-根据设计,正文内容被视为只能转发一次的只读流。
2016年

我猜这个问题更针对的是过滤器或中间件,而不是控制器。
Jim Aho

Answers:


111

在ASP.Net Core中,多次阅读正文请求似乎很复杂,但是,如果您的第一次尝试以正确的方式进行,那么接下来的尝试应该没问题。

我读了一些周到的例子,例如通过替换体流,但是我认为以下是最干净的:

最重要的一点是

  1. 让请求知道您将阅读其正文两次或更多次,
  2. 不关闭体液流,以及
  3. 将其倒带到其初始位置,这样内部流程就不会丢失。

[编辑]

正如Murad指出的那样,您还可以利用.Net Core 2.1扩展功能:EnableBuffering它将大型请求存储到磁盘上,而不是将其保留在内存中,从而避免了存储在内存中的大数据流问题(文件,图像等)。 。您可以通过设置ASPNETCORE_TEMP环境变量来更改临时文件夹,并在请求结束后删除文件。

在AuthorizationFilter中,您可以执行以下操作:

// Helper to enable request stream rewinds
using Microsoft.AspNetCore.Http.Internal;
[...]
public class EnableBodyRewind : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var bodyStr = "";
        var req = context.HttpContext.Request;

        // Allows using several time the stream in ASP.Net Core
        req.EnableRewind(); 

        // Arguments: Stream, Encoding, detect encoding, buffer size 
        // AND, the most important: keep stream opened
        using (StreamReader reader 
                  = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
        {
            bodyStr = reader.ReadToEnd();
        }

        // Rewind, so the core is not lost when it looks the body for the request
        req.Body.Position = 0;

        // Do whatever work with bodyStr here

    }
}



public class SomeController : Controller
{
    [HttpPost("MyRoute")]
    [EnableBodyRewind]
    public IActionResult SomeAction([FromBody]MyPostModel model )
    {
        // play the body string again
    }
}

然后,您可以在请求处理程序中再次使用主体。

在您的情况下,如果结果为空,则可能意味着该正文已在较早的阶段被读取。在这种情况下,您可能需要使用中间件(请参见下文)。

但是,如果要处理大数据流,则要小心,这种行为意味着所有内容都已加载到内存中,如果文件上传,则不应触发此操作。

您可能想将此用作中间件

我的看起来像这样(同样,如果您下载/上传大文件,则应禁用此选项以避免出现内存问题):

public sealed class BodyRewindMiddleware
{
    private readonly RequestDelegate _next;

    public BodyRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try { context.Request.EnableRewind(); } catch { }
        await _next(context);
        // context.Request.Body.Dipose() might be added to release memory, not tested
    }
}
public static class BodyRewindExtensions
{
    public static IApplicationBuilder EnableRequestBodyRewind(this IApplicationBuilder app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        return app.UseMiddleware<BodyRewindMiddleware>();
    }

}

流仍是空的,即使我倒回到位置0
阿德里安Nasui

2
你用过 req.EnableRewind();吗?我使用上面的代码,并且效果很好。
吉恩(Jean)

使用过req.EnableRewind(); 不起作用。我得到Position = 0,body length = 26,但是读取'body'流时出现一个空字符串。
阿德里安·纳苏

您必须在第一次读取正文之前启用重绕功能,而不是在第二次阅读之前启用重绕功能,如果您不这样做,我认为它将不起作用
Jean

3
也可以使用request.EnableBuffering()(包装EnableRewind())在ASP.NET Core 2.1中可用docs.microsoft.com/en-us/dotnet/api/…–
Murad,

27

在ASP.Net Core 2.1 / 3.1中更清晰的解决方案

过滤等级

using Microsoft.AspNetCore.Authorization;
// For ASP.NET 2.1
using Microsoft.AspNetCore.Http.Internal;
// For ASP.NET 3.1
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;

public class ReadableBodyStreamAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // For ASP.NET 2.1
        // context.HttpContext.Request.EnableRewind();
        // For ASP.NET 3.1
        // context.HttpContext.Request.EnableBuffering();
    }
}

在控制器中

[HttpPost]
[ReadableBodyStream]
public string SomePostMethod()
{
    //Note: if you're late and body has already been read, you may need this next line
    //Note2: if "Note" is true and Body was read using StreamReader too, then it may be necessary to set "leaveOpen: true" for that stream.
    HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);

    using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
    {
        string body = stream.ReadToEnd();
        // body = "param=somevalue&param2=someothervalue"
    }
}

2
对于netcore3.0,它将是.EnableBuffering()而不是.EnableRewind()
mr5,19年

感谢@ mr5-更新了我的答案
Andriod

我在修复某些.net Core 2.2-> Core 3.1升级时发现了这一问题,这些升级破坏了EnableRewind()的方式。我认为这需要再执行一行代码,否则我将无法重新阅读正文:HttpContext.Request.Body.Seek(0,SeekOrigin.Begin);
拉里·史密斯

2
仅在更改AuthorizeAttributeAttribute(在ASP.Net Core 3.1中)后,此功能才对我有用。
西格蒙德

伙计们请确保添加提到的库。我已经有了代码,但是EnableBuffering一直显示红色的波浪线,直到我意识到缺少Microsoft.AspNetCore.Http参考。多亏了Android!
kaarthick拉曼

18

为了回退请求正文,@ Jean的回答帮助我提出了一个似乎运行良好的解决方案。我目前将其用于全局异常处理程序中间件,但原理是相同的。

我创建了一个中间件,该中间件基本上可以使请求主体(而不是装饰器)上的倒带。

using Microsoft.AspNetCore.Http.Internal;
[...]
public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

    public EnableRequestRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

public static class EnableRequestRewindExtension
{
    public static IApplicationBuilder UseEnableRequestRewind(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<EnableRequestRewindMiddleware>();
    }
}

然后可以Startup.cs像这样使用它:

[...]
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    [...]
    app.UseEnableRequestRewind();
    [...]
}

使用这种方法,我能够成功倒带请求正文流。


1
这对我非常有用@SaoBiz 谢谢!一个错字,变2日构建器UseEnableRequestRewind(this IApplicationBuilder builder)
理查德·洛伍德

@RichardLogwood很高兴它有所帮助!感谢您找到错字!固定。:)
SaoBiz '17

6

这有点老了,但是自从我到达这里以来,我想我会发布我的发现,以便它们可以对其他人有所帮助。

首先,我遇到了同样的问题,我想要获取Request.Body并对此进行处理(记录/审核)。但是否则,我希望端点看起来相同。

因此,似乎EnableBuffering()调用可以解决问题。然后,您可以在正文上执行Seek(0,xxx)并重新读取内容,依此类推。

但是,这导致了我的下一个问题。访问端点时,会出现“不允许进行Synchornous操作”异常。因此,解决方法是在选项中设置属性AllowSynchronousIO = true。有多种方法可以完成此操作(但在此处不进行详细说明就不重要了。)

然后,下一个问题是,当我去阅读Request.Body时,它已经被处理掉了。啊。那么,有什么用呢?

我在endpiont调用中将Newtonsoft.JSON用作我的[FromBody]解析器。这就是负责同步读取的原因,并且它在完成时也会关闭流。解?在进行JSON解析之前阅读流吗?当然,那行得通,我最终得到了这一点:

 /// <summary>
/// quick and dirty middleware that enables buffering the request body
/// </summary>
/// <remarks>
/// this allows us to re-read the request body's inputstream so that we can capture the original request as is
/// </remarks>
public class ReadRequestBodyIntoItemsAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (context == null) return;

        // NEW! enable sync IO beacuse the JSON reader apparently doesn't use async and it throws an exception otherwise
        var syncIOFeature = context.HttpContext.Features.Get<IHttpBodyControlFeature>();
        if (syncIOFeature != null)
        {
            syncIOFeature.AllowSynchronousIO = true;

            var req = context.HttpContext.Request;

            req.EnableBuffering();

            // read the body here as a workarond for the JSON parser disposing the stream
            if (req.Body.CanSeek)
            {
                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))
                {
                    var jsonString = reader.ReadToEnd();

                    // store into the HTTP context Items["request_body"]
                    context.HttpContext.Items.Add("request_body", jsonString);
                }

                // go back to beginning so json reader get's the whole thing
                req.Body.Seek(0, SeekOrigin.Begin);
            }
        }
    }
}

因此,现在,我可以在具有[ReadRequestBodyIntoItems]属性的端点中使用HttpContext.Items [“ request_body”]访问正文。

但是,伙计,这似乎太多了。所以这是我结束的地方,对此我感到非常满意。

我的端点开始像这样:

[HttpPost("")]
[ReadRequestBodyIntoItems]
[Consumes("application/json")]
public async Task<IActionResult> ReceiveSomeData([FromBody] MyJsonObjectType value)
{
    val bodyString = HttpContext.Items["request_body"];
    // use the body, process the stuff...
}

但是,只需更改签名就更直接了,就像这样:

[HttpPost("")]
[Consumes("application/json")]
public async Task<IActionResult> ReceiveSomeData()
{
    using (var reader = new StreamReader(
           Request.Body,
           encoding: Encoding.UTF8,
           detectEncodingFromByteOrderMarks: false
    ))
    {
        var bodyString = await reader.ReadToEndAsync();

        var value = JsonConvert.DeserializeObject<MyJsonObjectType>(bodyString);

        // use the body, process the stuff...
    }
}

我真的很喜欢这个,因为它只读取一次体流,而且我可以控制反序列化。当然,如果ASP.NET core为我做到这一点很好,但是在这里我不会浪费时间两次读取流(也许每次都缓冲),并且代码非常清晰。

如果您在许多端点上都需要此功能,则中间件方法可能更简洁,或者至少可以将主体提取封装到扩展功能中,以使代码更简洁。

无论如何,我没有找到任何涉及此问题的所有三个方面的消息来源,因此这篇文章。希望这可以帮助某人!

顺便说一句:这是使用ASP .NET Core 3.1。


如果程序无法将JSON字符串解析为NyObjectType,那么我就无法从“ request_body”中读取值
Ericyu67

2

使用ASP.NET Core 2.1时遇到类似的问题:

  • 我需要一个自定义中间件来读取POST数据并对其执行一些安全检查
  • 使用授权过滤器不切实际,因为会影响大量操作
  • 我必须允许在操作中绑定对象([FromBody] someObject)。感谢您SaoBiz指出此解决方案。

因此,显而易见的解决方案是允许请求可重绕,但请确保在读取正文之后,绑定仍然有效。

EnableRequestRewindMiddleware

public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

    ///<inheritdoc/>
    public EnableRequestRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

启动文件

(将其放在Configure方法的开头)

app.UseMiddleware<EnableRequestRewindMiddleware>();

其他一些中间件

这是中间件的一部分,该中间件需要解压POST信息才能进行检查。

using (var stream = new MemoryStream())
{
    // make sure that body is read from the beginning
    context.Request.Body.Seek(0, SeekOrigin.Begin);
    context.Request.Body.CopyTo(stream);
    string requestBody = Encoding.UTF8.GetString(stream.ToArray());

    // this is required, otherwise model binding will return null
    context.Request.Body.Seek(0, SeekOrigin.Begin);
}

2

最近,我遇到了一个非常优雅的解决方案,它采用随机JSON,您不知道其结构:

    [HttpPost]
    public JsonResult Test([FromBody] JsonElement json)
    {
        return Json(json);
    }

就这么简单。


1

IHttpContextAccessor如果您想走这条路线,则该方法有效。

TLDR;

  • 注入 IHttpContextAccessor

  • 倒带- HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);

  • 阅读- System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body); JObject asObj = JObject.Parse(sr.ReadToEnd());

更多 -尝试简洁,不编译的示例示例,您需要确保这些示例已经准备就绪,以便可以使用IHttpContextAccessor。答案正确指出,当您尝试阅读请求正文时,您需要重新开始。的CanSeekPosition对请求主体的属性流来验证这很有帮助。

.NET Core DI文档

// First -- Make the accessor DI available
//
// Add an IHttpContextAccessor to your ConfigureServices method, found by default
// in your Startup.cs file:
// Extraneous junk removed for some brevity:
public void ConfigureServices(IServiceCollection services)
{
    // Typical items found in ConfigureServices:
    services.AddMvc(config => { config.Filters.Add(typeof(ExceptionFilterAttribute)); });
    // ...

    // Add or ensure that an IHttpContextAccessor is available within your Dependency Injection container
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

// Second -- Inject the accessor
//
// Elsewhere in the constructor of a class in which you want
// to access the incoming Http request, typically 
// in a controller class of yours:
public class MyResourceController : Controller
{
    public ILogger<PricesController> Logger { get; }
    public IHttpContextAccessor HttpContextAccessor { get; }

    public CommandController(
        ILogger<CommandController> logger,
        IHttpContextAccessor httpContextAccessor)
    {
        Logger = logger;
        HttpContextAccessor = httpContextAccessor;
    }

    // ...

    // Lastly -- a typical use 
    [Route("command/resource-a/{id}")]
    [HttpPut]
    public ObjectResult PutUpdate([FromRoute] string id, [FromBody] ModelObject requestModel)
    {
        if (HttpContextAccessor.HttpContext.Request.Body.CanSeek)
        {
            HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);
            System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body);
            JObject asObj = JObject.Parse(sr.ReadToEnd());

            var keyVal = asObj.ContainsKey("key-a");
        }
    }
}    

1

我能够像这样在asp.net核心3.1应用程序中读取请求正文(与一个简单的中间件一起启用缓冲-启用倒带似乎适用于早期.Net Core版本-):

var reader = await Request.BodyReader.ReadAsync();
Request.Body.Position = 0;
var buffer = reader.Buffer;
var body = Encoding.UTF8.GetString(buffer.FirstSpan);
Request.Body.Position = 0;

0

对于Body,您可以异步读取。

使用如下async方法:

public async Task<IActionResult> GetBody()
{
      string body="";
      using (StreamReader stream = new StreamReader(Request.Body))
      {
           body = await stream.ReadToEndAsync();
      }
    return Json(body);
}

用邮递员测试:

在此处输入图片说明

它运作良好,并已在Asp.net core版本中进行测试2.0 , 2.1 , 2.2, 3.0

我希望是有用的。


0

我还想阅读Request.Body而不自动将其映射到某些动作参数模型。解决此问题之前,测试了许多不同的方法。而且我在这里没有找到任何可行的解决方案。该解决方案当前基于.NET Core 3.0框架。

reader.readToEnd()的缝合方式很简单,即使它已编译,也抛出了运行时异常,这要求我使用异步调用。因此,我改用了ReadToEndAsync(),但是有时它可以工作,有时不能。给我类似错误,关闭流后无法读取。问题是我们不能保证它会在同一线程中返回结果(即使我们使用了等待)。因此,我们需要某种回调。这个解决方案对我有用。

[Route("[controller]/[action]")]
public class MyController : ControllerBase
{

    // ...

    [HttpPost]
    public async void TheAction()
    {
        try
        {
            HttpContext.Request.EnableBuffering();
            Request.Body.Position = 0;
            using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
            {
                var task = stream
                    .ReadToEndAsync()
                    .ContinueWith(t => {
                        var res = t.Result;
                        // TODO: Handle the post result!
                    });

                // await processing of the result
                task.Wait();
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to handle post!");
        }
    }

0

执行此操作的最简单方法如下:

  1. 在需要从中提取主体的Controller方法中,添加以下参数:[FromBody] SomeClass值

  2. 将“ SomeClass”声明为:class SomeClass {public string SomeParameter {get; 组; }}

当原始主体作为json发送时,.net核心知道如何非常轻松地读取它。


0

对于那些只想从请求中获取内容(请求主体)的用户:

[FromBody]在控制器方法参数中使用属性。

[Route("api/mytest")]
[ApiController]
public class MyTestController : Controller
{
    [HttpPost]
    [Route("content")]
    public async Task<string> ReceiveContent([FromBody] string content)
    {
        // Do work with content
    }
}

如doc所述:此属性指定应使用请求正文来绑定参数或属性。


0

在.NET Core 3.1中添加响应缓冲的快速方法是

    app.Use((context, next) =>
    {
        context.Request.EnableBuffering();
        return next();
    });

在Startup.cs中。我发现这也保证在读取流之前将启用缓冲,这对于.Net Core 3.1和我见过的其他一些中间件/授权过滤器答案是一个问题。

然后,您可以HttpContext.Request.Body像其他几个建议一样,通过处理程序读取请求正文。

还值得考虑的是,它EnableBuffering具有重载功能,允许您限制它在使用临时文件之前将在内存中缓冲多少,以及对缓冲区的总体限制。注意,如果请求超出此限制,则将引发异常,并且该请求将永远不会到达您的处理程序。


这对我来说非常出色(3.1)。引用了您一个不同的问题:stackoverflow.com/a/63525694/6210068
Lance
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.