如何读取 asp.net 核心 webapi 控制器中的请求主体?

我尝试在 OnActionExecuting方法中读取请求主体,但总是得到主体的 null

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

我尝试显式地将流的位置设置为0,但是这也不起作用。因为这是 ASP.NET 核心,所以我认为事情有点不同。我可以看到这里所有的示例都是关于旧的 web API 版本的。

还有别的办法吗?

299867 次浏览

在 ASP.Net Core 中,读取几次 body 请求似乎很复杂,但是,如果第一次尝试的方式正确,那么下次尝试应该没有问题。

我读过几个例子,通过替换身体流,但我认为以下是最干净的:

最重要的是

  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 at the body for the request
req.Body.Position = 0;


// Do whatever works with bodyStr here


}
}






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

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

在您的例子中,如果您得到一个 null 结果,这可能意味着主体已经在早期阶段被读取。在这种情况下,您可能需要使用一个中间件(见下文)。

但是,如果您处理大型流时要小心,这种行为意味着所有内存都已加载,在文件上传的情况下不应该触发这种行为。

您可能需要将其用作中间件

我的应用程序是这样的(如果你下载/上传大文件,应该禁用它以避免内存问题) :

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>();
}


}

“为了能够倒回请求主体,”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();
[...]
}

使用这种方法,我已经能够成功地倒带请求主体流。

如果您希望走这条路线,IHttpContextAccessor方法可以工作。

极低密度辐射;

  • 注射 IHttpContextAccessor

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

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

More ——为了获得一个可用的 IHttpContextAccessor,您需要确保项目的一个简洁的、非编译的示例。 答案已经正确地指出,当您尝试读取请求正文时,需要返回到开始。请求主体流上的 CanSeekPosition属性有助于验证这一点。

.NET 核心 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");
}
}
}

一个更清晰的解决方案,在 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"
}
}

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

  • 我需要一个自定义中间件来读取 POSTed 数据并对其执行一些安全检查
  • 由于受影响的操作数量很多,因此使用授权筛选器是不切实际的
  • 我必须允许动作中的对象绑定([ FromBody ] some Object)。感谢 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.EnableBuffering(); // this used to be EnableRewind
await _next(context);
}
}

Startup.cs

(将其放在 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);
}

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

使用 async方法如下:

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

邮递员测试:

enter image description here

它工作良好,并在 Asp.net core版本 2.0 , 2.1 , 2.2, 3.0测试。

希望能派上用场。

我还想读一下请求。身体没有自动地映射到一些行动参数模型。在解决这个问题之前测试了很多不同的方法。我在这里没有找到任何可行的解决方案。此解决方案当前基于。NET Core 3.0框架。

ReadToEnd ()看起来像一个简单的方法,即使它已经编译,它仍然抛出一个运行时异常,需要我使用异步调用。因此,我使用了 ReadToEndAsync () ,不过它有时候有用,有时候没用。给我的错误就像,流关闭后不能阅读。问题是,我们不能保证它将在同一个线程中返回结果(即使我们使用了 wait)。所以我们需要重新考虑一下。这个办法对我很有效。

[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!");
}
}

这是一个有点老的线索,但自从我来到这里,我想我会发布我的发现,以便他们可能会帮助其他人。

首先,我有同样的问题,我想得到的请求。主体,并对其进行某些操作(日志记录/审计)。但除此之外,我希望端点看起来一样。

因此,似乎 EnableBuffering ()调用可以解决这个问题。然后您可以在主体上执行 Seek (0,xxx)并重新读取内容,等等。

然而,这导致了我的下一个问题。当访问端点时,会出现“不允许同步操作”异常。因此,解决办法是在选项中设置属性 AllowSynchronousIO = true。有很多方法可以做到这一点(但这里不重要。.)

然后,下一个问题是,当我去读请求。尸体已经被处理掉了。呃。怎么回事?

我在端点调用中使用 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 because 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)
{
var 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 核心能为我做到这一点是很好的,但是在这里我不会浪费时间读两次流(也许每次都会缓冲) ,而且代码非常清晰和干净。

如果您需要在很多端点上使用这种功能,那么中间件方法可能会更简洁,或者您至少可以将正文提取封装到一个扩展函数中,以使代码更简洁。

不管怎样,我没有找到任何涉及这个问题所有3个方面的资料来源,因此这篇文章。希望这能帮到别人!

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

我能够在 asp.net 核心3.1应用程序中读取请求主体,就像这样(再加上一个简单的中间件,它支持缓冲-启用倒带,似乎可以在早期工作。网络核心版本 -) :

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;

最简单的方法如下:

  1. 在 Controller 方法中,您需要从中提取主体,添加以下参数: Some Class value

  2. 声明「有些类别」为: 类 { get; set; } }

当原始主体以 json 的形式发送时,. net core 知道如何轻松地读取它。

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

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

就这么简单。

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

在控制器方法参数中使用 [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 所说: this 属性指定应该使用请求主体绑定参数或属性。

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

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

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

然后,您可以通过 HttpContext.Request.Body在您的处理程序中读取您的请求正文,正如其他几个处理程序所建议的那样。

同样值得考虑的是,EnableBuffering具有重载,允许您在使用临时文件之前限制它在内存中的缓冲区数量,并且还限制了对缓冲区的总体限制。注意,如果一个请求超过这个限制,异常将被抛出,请求将永远不会到达您的处理程序。

这里有一个 POSTed JSON主体的解决方案,它不需要任何中间件或扩展,你只需覆盖 OnActionExecuting就可以访问主体中的所有数据集,甚至 URL 中的参数:

using System.Text.Json;


....


public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
    

// You can simply use filterContext.ActionArguments to get whatever param that you have set in the action
// For instance you can get the "json" param like this: filterContext.ActionArguments["json"]
// Or better yet just loop through the arguments and find the type
foreach(var elem in filterContext.ActionArguments)
{
if(elem.Value is JsonElement)
{
// Convert json obj to string
var json = ((JsonElement)elem.Value).GetRawText();
break;
}
}
}


[HttpPost]
public IActionResult Add([FromBody] JsonElement json, string id = 1)
{
return Ok("v1");
}

在我看来,编写扩展方法是最有效的方法

 public static string PeekBody(this HttpRequest request)
{
try
{
request.EnableBuffering();
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
request.Body.Read(buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
}
finally
{
request.Body.Position = 0;
}
}

你也可以使用 请求,尸体,偷窥者 Nuget 软件包(源代码)

//Return string
var request = HttpContext.Request.PeekBody();


//Return in expected type
LoginRequest request = HttpContext.Request.PeekBody<LoginRequest>();


//Return in expected type asynchronously
LoginRequest request = await HttpContext.Request.PeekBodyAsync<LoginRequest>();

我知道这是我的迟到,但在我的情况下,它只是我有一个问题在路由如下 在 startup.cs 文件中,我用/api 开始路由

app.MapWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")),
a =>
{
//if (environment.IsDevelopment())
//{
//  a.UseDeveloperExceptionPage();
//}


a.Use(async (context, next) =>
{
// API Call
context.Request.EnableBuffering();
await next();
});
//and I was putting in controller
[HttpPost]
[Route("/Register", Name = "Register")]
//Just Changed the rout to start with /api like my startup.cs file
[HttpPost]
[Route("/api/Register", Name = "Register")]
/and now the params are not null and I can ready the body request multiple

我在.NET5.0下遇到了同样的问题,上面的解决方案都不管用。 原来问题出在 Post 方法的返回值上。< strong > 必须是 Task 而不是 void。

错误代码:

[HttpPost]
public async void Post() {...}

好的代码:

[HttpPost]
public async Task Post() {...}