asp.netコアwebapiコントローラーでリクエスト本文を読み取る方法は?


105

OnActionExecutingメソッドでリクエストの本文を読み込もうとしていますが、常にnull本文を取得します。

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

ストリーム位置を明示的に0に設定しようとしましたが、それも機能しませんでした。これはASP.NET COREなので、状況は少し違うと思います。ここでは、すべてのサンプルが古いwebapiバージョンを参照しています。
これを行う他の方法はありますか?


4
リクエストパイプライン中にリクエストボディがすでに読み取られている場合、2回目に読み取ろうとすると空になります
Fabio


@Fabio情報をありがとう、位置を設定してもう一度読むことはできますか?
Kasun Koswattha 16年

@KasunKoswattha-設計上、本文のコンテンツは1回だけ読み取ることができる転送専用ストリームとして扱われます。
user270576

質問はコントローラーではなくフィルターやミドルウェアをターゲットにしていると思います。
ジム・アホ

回答:


111

ASP.Net Coreでは、ボディリクエストを数回読み取るのは複雑に見えますが、最初の試行が正しい方法でそれを行う場合は、次の試行で問題ないはずです。

たとえば、ボディストリームを置き換えることでいくつかのターンアラウンドを読みましたが、以下が最もクリーンだと思います。

最も重要な点は

  1. ボディを2回以上読み取ることをリクエストに通知するには、
  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();か?上記のコードを使用すると、うまく機能します。
ジャン

req.EnableRewind();を使用しています。動作しません。Position = 0、body length = 26を取得しましたが、「body」ストリームを読み取ると空の文字列が表示されます。
エイドリアンNasui

ボディが最初に読み込まれる前に巻き戻しを有効にし、2回目が読み込まれる前に巻き戻しを有効にする必要があります。それができない場合は、うまくいかないと思います
Jean

3
使用することも可能であるrequest.EnableBuffering()(ラッパー以上EnableRewind())これは、ASP.NETコア2.1で利用可能ですdocs.microsoft.com/en-us/dotnet/api/...
ミュラド

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

ありがとう@ mr5-私の回答を更新
Andriod

いくつかの.net Core 2.2-> Core 3.1アップグレードの修正中にこれが見つかりました。これにより、EnableRewind()の方法が壊れました。これにはもう1行のコードが必要だと思います。このコードがないと、本文を再度読み取ることができません。HttpContext.Request.Body.Seek(0、SeekOrigin.Begin);
ラリー・スミス

2
これはAuthorizeAttributeAttribute(ASP.Net Core 3.1で)に変更した後にのみ機能しました。
ジークムント

みんなplsは言及されたライブラリを追加することを確認します。私はすでにコードを持っていますが、Microsoft.AspNetCore.Http参照が欠落していることに気づくまで、EnableBufferingは赤い波線を表示していました。Androidに感謝!
カーシックラマン

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 ありがとうございます!1つのタイプミス、変更の第二へのビルダーUseEnableRequestRewind(this IApplicationBuilder builder)
リチャードログウッド

@RichardLogwoodそれが助けてくれてうれしい!タイプミスを見つけてくれてありがとう!修繕。:)
SaoBiz

6

これは少し古いスレッドですが、私がここに到着してから、他の人を助けるために私の発見を投稿すると思いました。

まず、同じ問題がありました。Request.Bodyを取得し、それを使って何かをしたいと考えていました(ロギング/監査)。しかし、それ以外の場合は、エンドポイントが同じに見えるようにしたかった。

したがって、EnableBuffering()呼び出しがうまくいくように思われました。次に、本文でSeek(0、xxx)を実行して、内容を再度読み取ることができます。

しかし、これが私の次の問題につながりました。エンドポイントにアクセスすると、「Synchornousオペレーションは許可されていません」という例外が発生します。したがって、回避策は、オプションでプロパティAllowSynchronousIO = trueを設定することです。これを行うにはいくつかの方法があります(ただし、ここで詳しく説明することは重要ではありません)。

次に、次の問題は、Request.Bodyを読みに行ったときにすでに破棄されていることです。ああ。だから、何を与えるのですか?

Endtonion呼び出しの[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...
    }
}

ボディストリームを1回だけ読み取るため、これが本当に好きで、逆シリアル化を制御できます。確かに、ASP.NETコアがこの魔法を使ってくれればいいのですが、ここではストリームを2回(おそらく毎回バッファリング)読み取るのに時間を無駄にしていません。コードは非常に明確でクリーンです。

多くのエンドポイントでこの機能が必要な場合は、ミドルウェアアプローチの方がクリーンである可能性があります。または、少なくとも本体の抽出を拡張関数にカプセル化して、コードをより簡潔にすることができます。

とにかく、私はこの問題の3つの側面すべてに触れたソースを見つけられなかったので、この投稿。うまくいけば、これは誰かを助けるでしょう!

ところで:これは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);
    }
}

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

最近、ランダムな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アプリケーションでリクエストボディを読み取ることができました(バッファリングを有効にする単純なミドルウェアと一緒に-巻き戻しを有効にすると、以前の。

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()を使用しましたが、機能する場合と機能しない場合があります。ストリームが閉じた後に読み取ることができないなどのエラーが表示されます。問題は、同じスレッドで結果が返されることを保証できないことです(たとえ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. 本体を抽出する必要があるコントローラーメソッドで、次のパラメーターを追加します:[FromBody] SomeClass value

  2. "SomeClass"を次のように宣言します:class SomeClass {public string SomeParameter {get; セットする; }}

raw bodyが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
    }
}

ドキュメントが言うように:この属性は、パラメーターまたはプロパティがリクエストの本文を使用してバインドされることを指定します。


0

.NET Core 3.1で応答バッファリングを追加する簡単な方法は、

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

Startup.cs。これは、ストリームが読み込まれる前にバッファリングが有効になることも保証することがわかりました。これは、私が見た他のミドルウェア/承認フィルターの回答のいくつかを持つ。

次にHttpContext.Request.Body、いくつかの他の人が提案しているように、ハンドラーでリクエストの本文を読み取ることができます。

またEnableBuffering、一時ファイルを使用する前にメモリにバッファリングする量を制限できるオーバーロードがあり、バッファの全体的な制限があることも考慮する価値があります。注意:リクエストがこの制限を超えると、例外がスローされ、リクエストはハンドラーに到達しません。


これは私にとっては見事に機能しました(3.1)。別の質問であなたを引用しました:stackoverflow.com/a/63525694/6210068
Lance
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.