我正在尝试用OnActionExecuting
方法阅读请求正文,但我总是为正文获取null
。
var request = context.HttpContext.Request;
var stream = new StreamReader(request.Body);
var body = stream.ReadToEnd();
我试图将流位置显式设置为0,但这也不起作用。由于这是ASP.NET CORE,我认为事情没有什么不同。我可以在这里看到所有的示例,指的是旧的webapi版本 还有其他方法吗?
答案 0 :(得分:77)
在ASP.Net Core中,读取身体请求的次数似乎很复杂,但是如果你的第一次尝试以正确的方式进行,那么下次尝试你应该没问题。
我通过替换身体流来阅读几个转变,但我认为以下是最干净的:
最重要的一点是
[编辑]
正如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
}
}
然后您可以在请求处理程序中再次使用正文。
在您的情况下,如果您获得null结果,则可能意味着已在较早阶段读取了正文。在这种情况下,您可能需要使用中间件(见下文)。
但是,如果你处理大流,要小心,这种行为意味着所有内容都被加载到内存中,在文件上传的情况下不应该触发。
Mine看起来像这样(再次,如果你下载/上传大文件,应该禁用它以避免内存问题):
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>();
}
}
答案 1 :(得分:14)
为了能够回复请求主体,@ 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();
[...]
}
使用这种方法,我能够成功地回滚请求正文流。
答案 2 :(得分:8)
这是一个旧线程,但是自从我到达这里以来,我想我会发布我的发现,以便它们可以帮助其他人。
首先,我遇到了同样的问题,我想要获取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内核为我做到这一点很不错,但是在这里我不会浪费时间两次读取流(也许每次都进行缓冲),并且代码非常清晰。
如果您需要在许多端点上使用此功能,则中间件方法可能更简洁,或者至少可以将主体提取封装到扩展功能中,以使代码更简洁。
无论如何,我没有找到涉及此问题所有三个方面的任何消息来源,因此,本文不存在。希望这对某人有帮助!
顺便说一句:这是使用ASP .NET Core 3.1。
答案 3 :(得分:5)
更清晰的解决方案,可在ASP.Net Core 2.1中使用。
过滤器类
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Filters;
public class ReadableBodyStreamAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
context.HttpContext.Request.EnableRewind();
}
}
在控制器中
[HttpPost]
[ReadableBodyStream]
public string SomePostMethod()
{
using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
{
string body = stream.ReadToEnd();
// body = "param=somevalue¶m2=someothervalue"
}
}
答案 4 :(得分:4)
在.NET Core 3.1中添加响应缓冲的快速方法是
app.Use((context, next) =>
{
context.Request.EnableBuffering();
return next();
});
在Startup.cs中。我发现这也保证在读取流之前将启用缓冲,这对于.Net Core 3.1和我见过的其他一些中间件/授权过滤器答案是一个问题。
然后,您可以像其他几个建议一样,通过处理程序中的HttpContext.Request.Body
读取请求正文。
还值得考虑的是,EnableBuffering
具有重载功能,可让您限制它在使用临时文件之前将在内存中缓冲多少,以及对缓冲区的总体限制。注意:如果请求超出此限制,则将引发异常,并且该请求将永远不会到达您的处理程序。
答案 5 :(得分:3)
最近,我遇到了一个非常优雅的解决方案,它采用随机JSON,您不知道其结构:
[HttpPost]
public JsonResult Test([FromBody] JsonElement json)
{
return Json(json);
}
就这么简单。
答案 6 :(得分:2)
我认为编写扩展方法是最有效的方法
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;
}
}
您也可以使用 Request.Body.Peeker Nuget 包 (source code)
//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>();
答案 7 :(得分: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
。
答案已正确指出,当您尝试阅读请求正文时,您需要回头查看。请求正文流上的CanSeek
,Position
属性有助于验证此内容。
// 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");
}
}
}
答案 8 :(得分:1)
我能够在asp.net core 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;
答案 9 :(得分:0)
使用ASP.NET Core 2.1时遇到类似的问题:
Dim a As Integer
Dim b As String
a = ThisWorkbook.ActiveSheet.Range("B1000000").End(xlUp).Row
b = ThisWorkbook.ActiveSheet.Range("B" & a).Value
指出了该解决方案。因此,显而易见的解决方案是允许请求可回绕,但请确保在读取正文之后,绑定仍然有效。
SaoBiz
(将其放在Configure方法的开头)
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);
}
}
这是中间件的一部分,该中间件需要解压POSTed信息才能进行检查。
app.UseMiddleware<EnableRequestRewindMiddleware>();
答案 10 :(得分: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
中进行了测试。
我希望是有用的。
答案 11 :(得分: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!");
}
}
答案 12 :(得分:0)
最简单的方法如下:
在需要从中提取主体的Controller方法中,添加以下参数: [FromBody] SomeClass值
将“ SomeClass”声明为: class SomeClass { 公共字符串SomeParameter {get;组; } }
当原始主体作为json发送时,.net核心知道如何非常轻松地读取它。
答案 13 :(得分: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
}
}
如文档所述:此属性指定应使用请求正文来绑定参数或属性。
答案 14 :(得分:0)
这是POST
版本的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");
}
答案 15 :(得分:-3)
显然我们可以使用IHttpContextAccessor来访问控制器中的http上下文。 只需要在启动类中注入并在控制器中获取它。
services.AddScoped<IHttpContextAccessor, HttpContextAccessor>();
使用它,您甚至可以在构造函数中访问上下文。