asp.net 코어 webapi 컨트롤러에서 요청 본문을 읽는 방법은 무엇입니까?


112

OnActionExecuting메서드 에서 요청 본문을 읽으려고 하지만 항상 null본문을 얻 습니다.

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

스트림 위치를 명시 적으로 0으로 설정하려고했지만 작동하지 않았습니다. 이것은 ASP.NET Core이므로 상황이 조금 다릅니다. 이전 웹 API 버전을 참조하는 모든 샘플을 여기에서 볼 수 있습니다.

이 작업을 수행하는 다른 방법이 있습니까?


5
당신이 그것을 두 번 읽으려고 할 때 요청 본문이 요청 파이프 라인 중 이미 전에 읽을 경우,주의, 그것은 비어
파비오


@Fabio 정보 주셔서 감사합니다. 위치를 설정하고 다시 읽을 수 있습니까?
Kasun Koswattha

@KasunKoswattha-설계 상 본문 내용은 한 번만 읽을 수있는 정방향 전용 스트림으로 처리됩니다.
user270576 2016

이 질문은 컨트롤러보다는 필터 나 미들웨어를 대상으로합니다.
Jim Aho

답변:


115

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
    }
}

그런 다음 요청 핸들러에서 본문을 다시 사용할 수 있습니다.

귀하의 경우 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>();
    }

}

나는 위치를 0으로 되 감을 경우 스트림에도 여전히 비어
아드리안 Nasui

2
사용 했습니까 req.EnableRewind();? 위의 코드를 사용하고 잘 작동합니다.

1
req.EnableRewind (); 작동하지 않습니다. Position = 0, body length = 26을 얻었지만 'body'스트림을 읽으면 빈 문자열이 나타납니다.
아드리안 Nasui

이것은 완벽한 해답입니다
Gayan

3
사용도 가능합니다 request.EnableBuffering()(래퍼 오버 EnableRewind()). ASP.NET Core 2.1 docs.microsoft.com/en-us/dotnet/api/…
Murad

29

보다 명확한 솔루션은 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 의 경우.EnableRewind()
mr5

감사합니다 @ MR5 - 내 대답을 업데이트
에서 Andriod

EnableRewind () 방식을 깨뜨린 일부 .net Core 2.2-> Core 3.1 업그레이드를 수정하는 동안 이것을 발견했습니다. 코드 한 줄이 더 필요하다고 생각합니다.이 코드 없이는 본문을 다시 읽을 수 없습니다. HttpContext.Request.Body.Seek (0, SeekOrigin.Begin);
Larry Smith

2
이것은 단지 변경 한 후 나를 위해 일 AuthorizeAttributeAttribute(3.1 ASP.Net 코어에서).
Sigmund

Guys pls는 언급 된 라이브러리를 추가해야합니다. 이미 코드가 있었지만 EnableBuffering은 Microsoft.AspNetCore.Http 참조가 누락되었음을 깨달을 때까지 빨간색 구불 구불 한 선을 표시했습니다. Android 덕분입니다!
kaarthick raman

18

요청 본문을 되감기 위해 @Jean의 대답은 잘 작동하는 것처럼 보이는 솔루션을 찾는 데 도움이되었습니다. 나는 현재 Global Exception Handler Middleware에 이것을 사용하지만 원칙은 동일합니다.

기본적으로 요청 본문 (데코레이터 대신)에서 되감기를 활성화하는 미들웨어를 만들었습니다.

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).
Richard Logwood 2017 년

@RichardLogwood 다행 이네요! 오타를 찾아 주셔서 감사합니다! 결정된. :)
SaoBiz

7

이것은 약간 오래된 스레드이지만 여기에 도착한 이후로 다른 사람들을 도울 수 있도록 내 결과를 게시 할 것이라고 생각했습니다.

먼저 Request.Body를 가져 와서 무언가를하고 싶었던 동일한 문제가있었습니다 (로깅 / 감사). 그러나 그렇지 않으면 끝 점이 똑같이 보이기를 원했습니다.

따라서 EnableBuffering () 호출이 트릭을 수행 할 수있는 것 같습니다. 그런 다음 본문에 대해 Seek (0, xxx)를 수행하고 내용을 다시 읽을 수 있습니다.

그러나 이것은 다음 문제로 이어졌습니다. 엔드 포인트에 액세스 할 때 "Synchornous operations are disallowed"예외가 발생합니다. 따라서 해결 방법은 옵션에서 AllowSynchronousIO = true 속성을 설정하는 것입니다. 이를 수행하는 방법에는 여러 가지가 있습니다 (하지만 여기서 자세히 설명하는 것은 중요하지 않습니다.).

그런 다음 다음 문제는 Request.Body를 읽으 러 가면 이미 폐기되었습니다. 으. 그래서, 무엇을 제공합니까?

Endpiont 호출에서 내 [FromBody] 파서로 Newtonsoft.JSON을 사용하고 있습니다. 이것이 동기 읽기를 담당하고 완료되면 스트림을 닫습니다. 해결책? 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 코어가이 마법을 사용하는 것이 좋지만 여기서는 스트림을 두 번 읽는 데 시간을 낭비하지 않고 (아마도 매번 버퍼링) 코드가 매우 명확하고 깨끗합니다.

많은 엔드 포인트에서이 기능이 필요한 경우 미들웨어 접근 방식이 더 깨끗할 수도 있고 적어도 본문 추출을 확장 함수로 캡슐화하여 코드를 더 간결하게 만들 수 있습니다.

어쨌든이 문제의 3 가지 측면을 모두 다룬 출처를 찾지 못했기 때문에이 게시물을 작성했습니다. 바라건대 이것은 누군가를 도울 것입니다!

BTW : 이것은 ASP .NET Core 3.1을 사용하고있었습니다.


프로그램이 JSON 문자열을 NyObjectType으로 구문 분석 할 수없는 경우 "request_body"에서 값을 읽을 수 없습니다
Ericyu67

3

최근에 나는 구조를 모르는 임의의 JSON을 취하는 매우 우아한 솔루션을 발견했습니다.

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

간단합니다.


감사합니다. 정말 효과가 있습니다. GetRawText()JsonElement 메서드를 사용 하고 JSON 텍스트를 받았습니다.
mrNone

DTO가 기본값 설정 등의 일부 처리를 수행하는 경우 실제 요청 본문을 제공하지 않습니다.
Ebram Shehata

3

.NET Core 3.1에서 응답 버퍼링을 추가하는 빠른 방법은 다음과 같습니다.

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

Startup.cs에서. 이것은 또한 스트림을 읽기 전에 버퍼링이 활성화된다는 것을 보장합니다. 이는 내가 본 다른 미들웨어 / 인증 필터 답변 중 일부와 함께 .Net Core 3.1의 문제였습니다.

그런 다음 HttpContext.Request.Body다른 여러 사람이 제안한 것처럼 처리기에서 요청 본문을 읽을 수 있습니다 .

또한 고려할 가치가 EnableBuffering있는 것은 임시 파일을 사용하기 전에 메모리에 버퍼링 할 양을 제한 할 수있는 오버로드와 전체 버퍼 제한이 있다는 것입니다. NB 요청이이 제한을 초과하면 예외가 발생하고 요청이 처리기에 도달하지 않습니다.


이것은 나를 위해 훌륭하게 작동했습니다 (3.1). 다른 질문에 대해 인용했습니다. stackoverflow.com/a/63525694/6210068
Lance

3.1에서 작업했습니다. 또한 이것을 사용할 사용자를위한 알림 : Startup.cs에 올바른 순서로 배치해야합니다.
Ebram Shehata

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

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

2

의 읽기를 위해 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.

유용하기를 바랍니다.


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 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;

0

또한 일부 작업 매개 변수 모델에 자동으로 매핑하지 않고 Request.Body를 읽고 싶었습니다. 이 문제를 해결하기 전에 여러 가지 방법을 테스트했습니다. 여기에 설명 된 작업 솔루션을 찾지 못했습니다. 이 솔루션은 현재 .NET Core 3.0 프레임 워크를 기반으로합니다.

reader.readToEnd ()는 간단한 방법처럼 이음새가 있지만 컴파일되었지만 비동기 호출을 사용해야하는 런타임 예외가 발생했습니다. 그래서 대신 ReadToEndAsync ()를 사용했지만 때로는 작동하고 때로는 작동하지 않았습니다. 나에게 오류를 주면 스트림이 닫힌 후 읽을 수 없습니다. 문제는 (await를 사용하더라도) 동일한 스레드에서 결과가 반환된다는 것을 보장 할 수 없다는 것입니다. 그래서 우리는 일종의 콜백이 필요합니다. 이 솔루션은 저에게 효과적이었습니다.

[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
    }
}

문서에서 알 수 있듯이이 속성은 요청 본문을 사용하여 매개 변수 또는 속성을 바인딩해야 함을 지정합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.