13

Ich habe gekämpft, um die Response.Body-Eigenschaft von einer ASP.NET Core-Aktion zu bekommen und die einzige Lösung, die ich identifizieren konnte, scheint eher hacky. Die Lösung erfordert das Austauschen von Response.Body mit einer MemoryStream beim Lesen des Streams in eine Zeichenfolge und dann das Austauschen vor dem Senden an den Client. In den folgenden Beispielen versuche ich den Wert Response.Body in einer benutzerdefinierten Middleware-Klasse zu erhalten. Response.Body ist eine einzige Eigenschaft in ASP.NET Core aus irgendeinem Grund gesetzt? Fehle ich hier etwas oder ist das ein Versager/Bug/Design-Problem? Gibt es eine bessere Möglichkeit, Response.Body zu lesen?Wie liest man ASP.NET Core Response.Body?

Hacky Lösung:

public class MyMiddleWare 
{ 
    private readonly RequestDelegate _next; 

    public MyMiddleWare(RequestDelegate next) 
    { 
     _next = next; 
    } 

    public async Task Invoke(HttpContext context) 
    { 
     using (var swapStream = new MemoryStream()) 
     { 
      var originalResponseBody = context.Response.Body; 

      context.Response.Body = swapStream; 

      await _next(context); 

      swapStream.Seek(0, SeekOrigin.Begin); 
      string responseBody = new StreamReader(swapStream).ReadToEnd(); 
      swapStream.Seek(0, SeekOrigin.Begin); 

      await swapStream .CopyToAsync(originalResponseBody); 
      context.Response.Body = originalResponseBody; 
     } 
    } 
} 

Versuchte Lösung mit EnableRewind(): funktioniert nur für Request.Body, nicht Response.Body. Dies führt dazu, dass ein leerer String von Response.Body gelesen wird und nicht der eigentliche Inhalt des Antworttexts (den ich mit PostMan verifiziert habe).

Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifeTime) 
{ 
    loggerFactory.AddConsole(Configuration.GetSection("Logging")); 
    loggerFactory.AddDebug(); 

    app.Use(async (context, next) => { 
     context.Request.EnableRewind(); 
     await next(); 
    }); 

    app.UseMyMiddleWare(); 

    app.UseMvc(); 

    // Dispose of Autofac container on application stop 
    appLifeTime.ApplicationStopped.Register(() => this.ApplicationContainer.Dispose()); 
} 

MyMiddleWare.cs

public class MyMiddleWare 
{ 
    private readonly RequestDelegate _next; 

    public MyMiddleWare(RequestDelegate next) 
    { 
     _next = next; 
    } 

    public async Task Invoke(HttpContext context) 
    { 
     await _next(context); 
     string responseBody = new StreamReader(context.Request.Body).ReadToEnd(); //responseBody is "" 
     context.Request.Body.Position = 0; 
    } 
} 
+0

, die durch Design ist. – Nkosi

Antwort

17

In meiner ursprünglichen Antwort, die ich auf die Frage völlig falsch verstanden hatte und dachte, das Plakat wurde gefragt, wie die Request.Body lesen Aber er hatte gefragt, wie man die Response.Body lesen könnte. Ich belasse meine ursprüngliche Antwort, um die Geschichte zu bewahren, aber ich aktualisiere sie auch, um zu zeigen, wie ich die Frage beantworten würde, sobald ich sie richtig gelesen habe.

Original-Antwort

Wenn Sie einen gepufferten Strom wollen, die mehrmals Sie

context.Request.EnableRewind() 

Im Idealfall tun, um diese früh in der Middleware festlegen müssen unterstützt lesen, bevor alles um den Körper lesen muss.

So zum Beispiel können Sie den folgenden Code am Anfang der Configure Methode der Startup.cs Datei platzieren können:

 app.Use(async (context, next) => { 
      context.Request.EnableRewind(); 
      await next(); 
     }); 

Vor ermöglicht Zurückspulen des Stroms mit den Request.Body verbunden ist, ist ein nach vorne nur, dass streamen unterstützt nicht das Suchen oder Lesen des Streams ein zweites Mal. Dies wurde getan, um die Standardkonfiguration der Anforderungsbehandlung so leicht und performant wie möglich zu gestalten. Sobald Sie jedoch den Rücklauf aktivieren, wird der Stream auf einen Stream aktualisiert, der das Suchen und Lesen mehrerer Streams unterstützt. Sie können dieses "Upgrade" beobachten, indem Sie einen Haltepunkt kurz vor und kurz nach dem Anruf auf EnableRewind setzen und die Eigenschaften Request.Body beobachten. So wird zum Beispiel Request.Body.CanSeek von false zu true wechseln.

Dann den Körper Stream lesen Sie zum Beispiel dies tun könnten:

obwohl
string bodyContent = new StreamReader(Request.Body).ReadToEnd(); 

nicht wickeln die StreamReader Schöpfung in einer using-Anweisung, oder es wird den darunter liegenden Körperstrom am Ende der Verwendung schließen Block und Code später im Anforderungslebenszyklus werden nicht in der Lage sein, den Körper zu lesen.

gerade auch sicher zu sein, könnte es eine gute Idee sein, die obige Codezeile zu folgen, die den Körper zufrieden mit dieser Codezeile liest die körpereigene Stromposition zurück auf 0.

request.Body.Position = 0; 

zurücksetzen Auf diese Weise wird jeder Code später im Anforderungslebenszyklus die Anforderung finden. Body in einem Zustand, wie er noch nicht gelesen wurde.

aktualisiert Antwort

Leider habe ich falsch verstanden Ihre Frage ursprünglich. Das Konzept, den zugehörigen Dampf zu einem gepufferten Dampf aufzurüsten, gilt immer noch. Wie auch immer, Sie müssen es manuell tun, mir ist keine integrierte .Net-Kernfunktionalität bekannt, mit der Sie den Antwortstream einmal lesen können, so wie ein Entwickler den Request-Stream lesen kann, nachdem er gelesen wurde.

Ihr "hacky" Ansatz ist wahrscheinlich völlig angemessen. Sie konvertieren im Grunde einen Stream, der nicht zu einem suchen kann. Am Ende des Tages muss der Stream Response.Body mit einem gepufferten Stream ausgetauscht werden, der das Suchen unterstützt. Hier ist eine andere Ansicht von Middleware, um das zu tun, aber Sie werden bemerken, dass es Ihrem Ansatz ziemlich ähnlich ist. Ich entschied mich jedoch, einen finally-Block als zusätzlichen Schutz zu verwenden, um den ursprünglichen Stream wieder auf Response.Body zu setzen, und ich verwendete die Position Eigenschaft des Streams anstelle der Seek Methode, da die Syntax ein wenig einfacher ist, aber der Effekt ist nicht anders als Ihr Ansatz.

public class ResponseRewindMiddleware { 
     private readonly RequestDelegate next; 

     public ResponseRewindMiddleware(RequestDelegate next) { 
      this.next = next; 
     } 

     public async Task Invoke(HttpContext context) { 

      Stream originalBody = context.Response.Body; 

      try { 
       using (var memStream = new MemoryStream()) { 
        context.Response.Body = memStream; 

        await next(context); 

        memStream.Position = 0; 
        string responseBody = new StreamReader(memStream).ReadToEnd(); 

        memStream.Position = 0; 
        await memStream.CopyToAsync(originalBody); 
       } 

      } finally { 
       context.Response.Body = originalBody; 
      } 

     } 
+0

So fand ich diese Dokumentation auf EnableRewind(): [MS Docs] (https://docs.microsoft.com/en-us/aspnet/core/api/microsoft.aspnetcore.http.internal.bufferinghelper), aber es tut nicht Es gibt wirklich viel Hinweis darauf, wie es funktioniert ... Können Sie näher ausführen, wie ich meinen Code ändern könnte, um EnableRewind zu nutzen? –

+0

Sicher, ich werde mehr zu meiner Antwort hinzufügen. Ich benutze es und es funktioniert großartig. –

+1

Danke für die zusätzlichen Informationen @Ron C, ich habe versucht, die Lösung, die Sie vorgeschlagen, und während ich sehen kann, dass die CanRead und CanSeek Eigenschaften auf True aktualisiert wurden, liest der Leser einfach eine leere Zeichenfolge zurück in die Variable bodyContent. Ich kann in PostMan sehen, dass ein tatsächlicher vollständiger Antworttext an den Client zurückgegeben wird. Ich werde meine Frage aktualisieren, um meinen Ansatz mit EnableRewind() widerzuspiegeln. –

4

Was Sie als einen Hack beschreiben, ist eigentlich der vorgeschlagene Ansatz, wie Antwort Streams in benutzerdefinierten Middleware verwaltet werden.

Aufgrund der Pipeline-Natur des Middleware-Designs, bei dem jede Middleware den vorherigen oder nächsten Handler in der Pipeline nicht kennt. Es gibt keine Garantie dafür, dass die aktuelle Middleware die Antwort schreiben würde, wenn sie nicht den Antwortstrom behält, den sie erhalten hat, bevor sie einen Datenstrom weitergibt, den sie (die aktuelle Middleware) kontrolliert. Dieser Entwurf wurde in OWIN gesehen und schließlich in asp.net-Kern gebacken.

Sobald Sie mit dem Schreiben in den Antwort-Stream beginnen, werden der Nachrichtentext und die Header (die Antwort) an den Client gesendet. Wenn ein anderer Handler die Pipeline herunterfährt, bevor der aktuelle Handler eine Chance hatte, kann er der Antwort nichts hinzufügen, nachdem sie bereits gesendet wurde.

Es ist wiederum nicht garantiert, dass es sich um den tatsächlichen Antwortstream handelt, wenn die vorherige Middleware in der Pipeline der gleichen Strategie folgte, einen weiteren Stream auf der Leitung zu übergeben.

Referenzierung ASP.NET Core Middleware Fundamentals

Warnung

Seien Sie vorsichtig die HttpResponse nach der Änderung next Aufruf, weil die Antwort bereits an den Client gesendet worden sein kann. Sie können HttpResponse.HasStarted verwenden, um zu überprüfen, ob die Header gesendet wurden.

Warnung

nicht next.Invoke rufen Sie nach einer write Methode aufrufen. Eine Middleware Komponente erzeugt entweder eine Antwort oder ruft next.Invoke auf, aber nicht .

Beispiel eingebauten Grundmiddle von aspnet/BasicMiddleware Github repo

ResponseCompressionMiddleware.cs

/// <summary> 
/// Invoke the middleware. 
/// </summary> 
/// <param name="context"></param> 
/// <returns></returns> 
public async Task Invoke(HttpContext context) 
{ 
    if (!_provider.CheckRequestAcceptsCompression(context)) 
    { 
     await _next(context); 
     return; 
    } 

    var bodyStream = context.Response.Body; 
    var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>(); 
    var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>(); 

    var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider, 
     originalBufferFeature, originalSendFileFeature); 
    context.Response.Body = bodyWrapperStream; 
    context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream); 
    if (originalSendFileFeature != null) 
    { 
     context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream); 
    } 

    try 
    { 
     await _next(context); 
     // This is not disposed via a using statement because we don't want to flush the compression buffer for unhandled exceptions, 
     // that may cause secondary exceptions. 
     bodyWrapperStream.Dispose(); 
    } 
    finally 
    { 
     context.Response.Body = bodyStream; 
     context.Features.Set(originalBufferFeature); 
     if (originalSendFileFeature != null) 
     { 
      context.Features.Set(originalSendFileFeature); 
     } 
    } 
} 
Verwandte Themen