Как читать тело запроса в главном контроллере webapi asp.net?
Я пытаюсь прочитать тело запроса в методе OnActionExecuting
, но я всегда получаю null
для тела.
var request = context.HttpContext.Request;
var stream = new StreamReader(request.Body);
var body = stream.ReadToEnd();
Я попытался явно установить позицию потока в 0, но это также не сработало. Так как это ASP.NET CORE, все выглядит немного по-другому. Я вижу все образцы здесь, ссылаясь на старые версии webapi.
Есть ли другой способ сделать это?
Ответы
Ответ 1
По-видимому, мы можем использовать IHttpContextAccessor для доступа к http-контексту в контроллерах.
Просто нужно ввести в класс запуска и получить его в своих контроллерах.
services.AddScoped<IHttpContextAccessor, HttpContextAccessor>();
с помощью этого вы можете получить доступ к контексту даже в конструкторе.
Ответ 2
В ASP.Net Core кажется сложным несколько раз прочитать запрос тела, однако, если ваша первая попытка делает это правильно, вам будет хорошо при следующих попытках.
Я прочитал несколько оборотов, например, заменив поток тела, но я думаю, что следующее является самым чистым:
Наиболее важными моментами являются
- сообщить запросу, что вы будете читать его тело дважды или более,
- не закрывать поток тела, и
- перемотать его в исходное положение, чтобы внутренний процесс не потерялся.
[EDIT]
Как отметил Мурад, вы также можете воспользоваться преимуществами расширения .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>();
}
}
Ответ 3
Чтобы иметь возможность перемотать тело запроса, ответ @Jean помог мне придумать решение, которое, кажется, работает хорошо. В настоящее время я использую это для промежуточного программного обеспечения Global Exception Handler, но принцип тот же.
Я создал промежуточное программное обеспечение, которое в основном позволяет перемотку на тело запроса (вместо декоратора).
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();
[...]
}
Используя этот подход, я смог успешно перемотать поток тела запроса.
Ответ 4
Более четкое решение работает в 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"
}
}
Ответ 5
Метод IHttpContextAccessor
работает, если вы хотите пройти этот маршрут.
TL;DR;
-
Внедрить 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());
System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body); JObject asObj = JObject.Parse(sr.ReadToEnd());
Подробнее - попытка получить сжатый, не компилируемый пример элементов, которые вам понадобятся для обеспечения, чтобы получить IHttpContextAccessor
. Ответы правильно указывали, что вам нужно будет вернуться к началу, когда вы попытаетесь прочитать тело запроса. CanSeek
, Position
в потоке тела запроса полезны для проверки этого.
.NET Core DI Docs
// 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");
}
}
}
Ответ 6
У меня была похожая проблема при использовании 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);
}
}
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);
}