Anforderungs- und Antwortvorgänge in ASP.NET Core
Hinweis
Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
Warnung
Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der Supportrichtlinie für .NET und .NET Core. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
Wichtig
Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.
Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
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.