Anforderungs- und Antwortvorgänge in ASP.NET CoreRequest and response operations in ASP.NET Core

Von Justin KotalikBy Justin Kotalik

In diesem Artikel wird erläutert, wie Sie den Anforderungstext lesen und den Antworttext schreiben.This article explains how to read from the request body and write to the response body. Möglicherweise ist Code für diese Vorgänge erforderlich, wenn Sie Middleware schreiben.Code for these operations might be required when writing middleware. Abgesehen vom Schreiben von Middleware ist benutzerdefinierter Code in der Regel nicht erforderlich, da die Vorgänge von MVC und Razor Pages behandelt werden.Outside of writing middleware, custom code isn't generally required because the operations are handled by MVC and Razor Pages.

Es stehen zwei Abstraktionen für die Anforderungs- und Antworttexte zur Verfügung: Stream und Pipe.There are two abstractions for the request and response bodies: Stream and Pipe. HttpRequest.Body ist eine Stream-Klasse zum Lesen von Anforderungen, und HttpRequest.BodyReader ist eine PipeReader-Klasse.For request reading, HttpRequest.Body is a Stream, and HttpRequest.BodyReader is a PipeReader. HttpResponse.Body ist eine Stream-Klasse zum Schreiben von Antworten, und HttpResponse.BodyWriter ist eine PipeWriter-Klasse.For response writing, HttpResponse.Body is a Stream, and HttpResponse.BodyWriter is a PipeWriter.

Anstelle von Datenströmen werden Pipelines empfohlen.Pipelines are recommended over streams. Datenströme können bei einigen einfachen Vorgängen einfacher zu verwenden sein, aber Pipelines haben einen Leistungsvorteil und sind in den meisten Szenarien einfacher zu verwenden.Streams can be easier to use for some simple operations, but pipelines have a performance advantage and are easier to use in most scenarios. ASP.NET Core beginnt, intern Pipelines anstelle von Datenströmen zu verwenden.ASP.NET Core is starting to use pipelines instead of streams internally. Beispiele:Examples include:

  • FormReader
  • TextReader
  • TextWriter
  • HttpResponse.WriteAsync

Datenströme werden jedoch nicht aus dem Framework entfernt.Streams aren't being removed from the framework. Datenströme werden weiterhin in .NET verwendet, und für viele Datenstromtypen gibt es keine entsprechenden Pipelines, z. B. FileStreams und ResponseCompression.Streams continue to be used throughout .NET, and many stream types don't have pipe equivalents, such as FileStreams and ResponseCompression.

DatenstrombeispieleStream examples

Angenommen, Sie möchten eine Middleware erstellen, die den gesamten Anforderungstext als Liste von Zeichenfolgen liest, die in neue Zeilen umbrochen werden.Suppose the goal is to create a middleware that reads the entire request body as a list of strings, splitting on new lines. Eine einfache Datenstromimplementierung könnte wie im folgenden Beispiel aussehen:A simple stream implementation might look like the following example:

Warnung

Der folgende CodeThe following code:

  • Der Code wird verwendet, um die Probleme zu veranschaulichen, bei denen keine Pipe zum Lesen des Anforderungstexts verwendet wird.Is used to demonstrate the problems with not using a pipe to read the request body.
  • Der Code ist nicht für die Verwendung in Produktions-Apps vorgesehen.Is not intended to be used in production apps.
private async Task<List<string>> GetListOfStringsFromStream(Stream requestBody)
{
    // Build up the request body in a string builder.
    StringBuilder builder = new StringBuilder();

    // Rent a shared buffer to write the request body into.
    byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

    while (true)
    {
        var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0, buffer.Length);
        if (bytesRemaining == 0)
        {
            break;
        }

        // Append the encoded string into the string builder.
        var encodedString = Encoding.UTF8.GetString(buffer, 0, bytesRemaining);
        builder.Append(encodedString);
    }

    ArrayPool<byte>.Shared.Return(buffer);

    var entireRequestBody = builder.ToString();

    // Split on \n in the string.
    return new List<string>(entireRequestBody.Split("\n"));
}

Wenn Sie möchten, dass Codekommentare in anderen Sprachen als Englisch angezeigt werden, informieren Sie uns in diesem GitHub-Issue.If you would like to see code comments translated to languages other than English, let us know in this GitHub discussion issue.

Dieser Code funktioniert, aber es gibt einige Probleme:This code works, but there are some issues:

  • Vor dem Anfügen an den StringBuilder wird im Beispiel eine andere Zeichenfolge erstellt (encodedString), die sofort verworfen wird.Before appending to the StringBuilder, the example creates another string (encodedString) that is thrown away immediately. Weil dieser Vorgang für alle Bytes im Datenstrom erfolgt, ist das Ergebnis eine zusätzliche Speicherzuweisung in der Größe des gesamten Anforderungstexts.This process occurs for all bytes in the stream, so the result is extra memory allocation the size of the entire request body.
  • Im Beispiel wird die gesamte Zeichenfolge vor dem Umbruch in neue Zeilen gelesen.The example reads the entire string before splitting on new lines. Es ist viel effizienter, das Bytearray auf neue Zeilen zu überprüfen.It's more efficient to check for new lines in the byte array.

Es folgt ein Beispiel, in dem einige dieser Probleme behoben werden:Here's an example that fixes some of the preceding issues:

Warnung

Der folgende CodeThe following code:

  • Der Code wird verwendet, um die Lösungen für einige Probleme im vorangehenden Code zu veranschaulichen, ohne dabei alle Probleme zu lösen.Is used to demonstrate the solutions to some problems in the preceding code while not solving all the problems.
  • Der Code ist nicht für die Verwendung in Produktions-Apps vorgesehen.Is not intended to be used in production apps.
private async Task<List<string>> GetListOfStringsFromStreamMoreEfficient(Stream requestBody)
{
    StringBuilder builder = new StringBuilder();
    byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
    List<string> results = new List<string>();

    while (true)
    {
        var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0, buffer.Length);

        if (bytesRemaining == 0)
        {
            results.Add(builder.ToString());
            break;
        }

        // Instead of adding the entire buffer into the StringBuilder
        // only add the remainder after the last \n in the array.
        var prevIndex = 0;
        int index;
        while (true)
        {
            index = Array.IndexOf(buffer, (byte)'\n', prevIndex);
            if (index == -1)
            {
                break;
            }

            var encodedString = Encoding.UTF8.GetString(buffer, prevIndex, index - prevIndex);

            if (builder.Length > 0)
            {
                // If there was a remainder in the string buffer, include it in the next string.
                results.Add(builder.Append(encodedString).ToString());
                builder.Clear();
            }
            else
            {
                results.Add(encodedString);
            }

            // Skip past last \n
            prevIndex = index + 1;
        }

        var remainingString = Encoding.UTF8.GetString(buffer, prevIndex, bytesRemaining - prevIndex);
        builder.Append(remainingString);
    }

    ArrayPool<byte>.Shared.Return(buffer);

    return results;
}

Dieses obige Beispiel:This preceding example:

  • Puffert nicht den gesamten Anforderungstext in einem StringBuilder, wenn keine Zeilenvorschubzeichen vorhanden sind.Doesn't buffer the entire request body in a StringBuilder unless there aren't any newline characters.
  • Ruft nicht Split für die Zeichenfolge auf.Doesn't call Split on the string.

Es gibt jedoch noch ein paar Probleme:However, there are still are a few issues:

  • Wenn nur wenige neue Zeilenvorschubzeichen vorliegen, wird ein großer Teil des Anforderungstexts in der Zeichenfolge gepuffert.If newline characters are sparse, much of the request body is buffered in the string.
  • Der Code erstellt weiterhin Zeichenfolgen (remainingString) und fügt diese zum Zeichenfolgenpuffer hinzu, was zu einer weiteren Zuteilung führt.The code continues to create strings (remainingString) and adds them to the string buffer, which results in an extra allocation.

Diese Probleme können behoben werden, aber der Code wird immer komplizierter, und die Verbesserung ist gering.These issues are fixable, but the code is becoming progressively more complicated with little improvement. Pipelines bieten eine Möglichkeit, diese Probleme mit minimaler Codekomplexität zu lösen.Pipelines provide a way to solve these problems with minimal code complexity.

PipelinesPipelines

Das folgende Beispiel zeigt, wie das gleiche Szenario mit PipeReader behandelt werden kann:The following example shows how the same scenario can be handled using a PipeReader:

private async Task<List<string>> GetListOfStringFromPipe(PipeReader reader)
{
    List<string> results = new List<string>();

    while (true)
    {
        ReadResult readResult = await reader.ReadAsync();
        var buffer = readResult.Buffer;

        SequencePosition? position = null;

        do
        {
            // Look for a EOL in the buffer
            position = buffer.PositionOf((byte)'\n');

            if (position != null)
            {
                var readOnlySequence = buffer.Slice(0, position.Value);
                AddStringToList(results, in readOnlySequence);

                // Skip the line + the \n character (basically position)
                buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
            }
        }
        while (position != null);


        if (readResult.IsCompleted && buffer.Length > 0)
        {
            AddStringToList(results, in buffer);
        }

        reader.AdvanceTo(buffer.Start, buffer.End);

        // At this point, buffer will be updated to point one byte after the last
        // \n character.
        if (readResult.IsCompleted)
        {
            break;
        }
    }

    return results;
}

private static void AddStringToList(List<string> results, in ReadOnlySequence<byte> readOnlySequence)
{
    // Separate method because Span/ReadOnlySpan cannot be used in async methods
    ReadOnlySpan<byte> span = readOnlySequence.IsSingleSegment ? readOnlySequence.First.Span : readOnlySequence.ToArray().AsSpan();
    results.Add(Encoding.UTF8.GetString(span));
}

In diesem Beispiel werden viele Probleme der Datenstromimplementierungen behoben:This example fixes many issues that the streams implementations had:

  • Ein Zeichenfolgenpuffer ist nicht erforderlich, weil der PipeReader Bytes behandelt, die nicht verwendet wurden.There's no need for a string buffer because the PipeReader handles bytes that haven't been used.
  • Codierte Zeichenfolgen werden direkt der Liste der zurückgegebenen Zeichenfolgen hinzugefügt.Encoded strings are directly added to the list of returned strings.
  • Die Erstellung von Zeichenfolgen erfolgt mit Ausnahme des ToArray-Aufrufs und des von der Zeichenfolge verwendeten Speichers ohne Zuordnung.Other than the ToArray call, and the memory used by the string, string creation is allocation free.

AdapterAdapters

Die Eigenschaften Body, BodyReader und BodyWriter sind für HttpRequest und HttpResponse verfügbar.The Body, BodyReader, and BodyWriter properties are available for HttpRequest and HttpResponse. Wenn Sie für Body einen anderen Datenstrom festlegen, passen neue Adapter die Typen automatisch aneinander an.When you set Body to a different stream, a new set of adapters automatically adapt each type to the other. Wenn Sie für HttpRequest.Body einen neuen Datenstrom festlegen, wird für HttpRequest.BodyReader automatisch ein neuer PipeReader festgelegt, der HttpRequest.Body umschließt.If you set HttpRequest.Body to a new stream, HttpRequest.BodyReader is automatically set to a new PipeReader that wraps HttpRequest.Body.

StartAsyncStartAsync

HttpResponse.StartAsync wird verwendet, um anzugeben, dass Header nicht änderbar sind, und um OnStarting-Rückrufe auszuführen.HttpResponse.StartAsync is used to indicate that headers are unmodifiable and to run OnStarting callbacks. Bei der Verwendung von Kestrel als Server garantiert der Aufruf von StartAsync vor der Verwendung von PipeReader, dass von GetMemory zurückgegebener Arbeitsspeicher zu internen Pipe-Pipeline von Kestrel gehört, anstatt einem externen Puffer.When using Kestrel as a server, calling StartAsync before using the PipeReader guarantees that memory returned by GetMemory belongs to Kestrel's internal Pipe rather than an external buffer.

Zusätzliche RessourcenAdditional resources