Anforderungs- und Antwortvorgänge in ASP.NET Core

Von Justin Kotalik

In diesem Artikel wird erläutert, wie Sie den Anforderungstext lesen und den Antworttext schreiben. Möglicherweise ist Code für diese Vorgänge erforderlich, wenn Sie Middleware schreiben. Abgesehen vom Schreiben von Middleware ist benutzerdefinierter Code in der Regel nicht erforderlich, da die Vorgänge von MVC und Razor Pages behandelt werden.

Es stehen zwei Abstraktionen für die Anforderungs- und Antworttexte zur Verfügung: Stream und Pipe. HttpRequest.Body ist eine Stream-Klasse zum Lesen von Anforderungen, und HttpRequest.BodyReader ist eine PipeReader-Klasse. HttpResponse.Body ist eine Stream-Klasse zum Schreiben von Antworten, und HttpResponse.BodyWriter ist eine PipeWriter-Klasse.

Anstelle von Datenströmen werden Pipelines empfohlen. 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. ASP.NET Core beginnt, intern Pipelines anstelle von Datenströmen zu verwenden. Beispiele:

  • FormReader
  • TextReader
  • TextWriter
  • HttpResponse.WriteAsync

Datenströme werden jedoch nicht aus dem Framework entfernt. Datenströme werden weiterhin in .NET verwendet, und für viele Datenstromtypen gibt es keine entsprechenden Pipelines, z. B. FileStreams und ResponseCompression.

Datenstrombeispiele

Angenommen, Sie möchten eine Middleware erstellen, die den gesamten Anforderungstext als Liste von Zeichenfolgen liest, die in neue Zeilen umbrochen werden. Eine einfache Datenstromimplementierung könnte wie im folgenden Beispiel aussehen:

Warnung

Der folgende Code

  • Der Code wird verwendet, um die Probleme zu veranschaulichen, bei denen keine Pipe zum Lesen des Anforderungstexts verwendet wird.
  • Der Code ist nicht für die Verwendung in Produktions-Apps vorgesehen.
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.

Dieser Code funktioniert, aber es gibt einige Probleme:

  • Vor dem Anfügen an den StringBuilder wird im Beispiel eine andere Zeichenfolge erstellt (encodedString), die sofort verworfen wird. Weil dieser Vorgang für alle Bytes im Datenstrom erfolgt, ist das Ergebnis eine zusätzliche Speicherzuweisung in der Größe des gesamten Anforderungstexts.
  • Im Beispiel wird die gesamte Zeichenfolge vor dem Umbruch in neue Zeilen gelesen. Es ist viel effizienter, das Bytearray auf neue Zeilen zu überprüfen.

Es folgt ein Beispiel, in dem einige dieser Probleme behoben werden:

Warnung

Der folgende 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.
  • Der Code ist nicht für die Verwendung in Produktions-Apps vorgesehen.
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:

  • Puffert nicht den gesamten Anforderungstext in einem StringBuilder, wenn keine Zeilenvorschubzeichen vorhanden sind.
  • Ruft nicht Split für die Zeichenfolge auf.

Es gibt jedoch noch einige Probleme:

  • Wenn nur wenige neue Zeilenvorschubzeichen vorliegen, wird ein großer Teil des Anforderungstexts in der Zeichenfolge gepuffert.
  • Der Code erstellt weiterhin Zeichenfolgen (remainingString) und fügt diese zum Zeichenfolgenpuffer hinzu, was zu einer weiteren Zuteilung führt.

Diese Probleme können behoben werden, aber der Code wird immer komplizierter, und die Verbesserung ist gering. Pipelines bieten eine Möglichkeit, diese Probleme mit minimaler Codekomplexität zu lösen.

Pipelines

Das folgende Beispiel zeigt, wie das gleiche Szenario mit PipeReader behandelt werden kann:

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:

  • Ein Zeichenfolgenpuffer ist nicht erforderlich, weil der PipeReader Bytes behandelt, die nicht verwendet wurden.
  • Codierte Zeichenfolgen werden direkt der Liste der zurückgegebenen Zeichenfolgen hinzugefügt.
  • Die Erstellung von Zeichenfolgen erfolgt mit Ausnahme des ToArray-Aufrufs und des von der Zeichenfolge verwendeten Speichers ohne Zuordnung.

Adapter

Die Eigenschaften Body, BodyReader und BodyWriter sind für HttpRequest und HttpResponse verfügbar. Wenn Sie für Body einen anderen Datenstrom festlegen, passen neue Adapter die Typen automatisch aneinander an. 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.

StartAsync

HttpResponse.StartAsync wird verwendet, um anzugeben, dass Header nicht änderbar sind, und um OnStarting-Rückrufe auszuführen. 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.

Zusätzliche Ressourcen