Operazioni di richiesta e risposta in ASP.NET Core
Nota
Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 8 di questo articolo.
Avviso
Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere Criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 8 di questo articolo.
Importante
Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.
Per la versione corrente, vedere la versione .NET 8 di questo articolo.
Questo articolo illustra come leggere dal corpo della richiesta e scrivere nel corpo della risposta. Il codice per queste operazioni potrebbe essere necessario durante la scrittura del middleware. Al di fuori della scrittura del middleware, il codice personalizzato non è in genere necessario perché le operazioni vengono gestite da MVC e Razor Pages.
Esistono due astrazioni per i corpi di richiesta e risposta: Stream e Pipe. Per la lettura delle richieste, HttpRequest.Body è un Streamoggetto e HttpRequest.BodyReader
è un oggetto PipeReader. Per la scrittura delle risposte, HttpResponse.Body è un Streamoggetto e HttpResponse.BodyWriter
è un oggetto PipeWriter.
Le pipeline sono consigliate su flussi. I flussi possono essere più facili da usare per alcune operazioni semplici, ma le pipeline hanno prestazioni migliori e sono più facili da usare nella maggior parte degli scenari. ASP.NET Core sta iniziando a usare pipeline anziché flussi internamente. Alcuni esempi:
FormReader
TextReader
TextWriter
HttpResponse.WriteAsync
I flussi non vengono rimossi dal framework. I flussi continuano a essere usati in .NET e molti tipi di flusso non hanno equivalenti pipe, ad esempio FileStreams
e ResponseCompression
.
Esempi di flussi
Si supponga che l'obiettivo sia quello di creare un middleware che legge l'intero corpo della richiesta come elenco di stringhe, suddividendo su nuove righe. Un'implementazione di flusso semplice potrebbe essere simile alla seguente:
Avviso
Il codice seguente:
- Viene usato per illustrare i problemi relativi all'uso di una pipe per leggere il corpo della richiesta.
- Non deve essere usato nelle app di produzione.
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"));
}
Per visualizzare i commenti del codice tradotti in lingue diverse dall'inglese, segnalarlo in questo problema di discussione su GitHub.
Questo codice funziona, ma esistono alcuni problemi:
- Prima dell'aggiunta a
StringBuilder
, l'esempio crea un'altra stringa (encodedString
) che viene eliminata immediatamente. Questo processo si verifica per tutti i byte nel flusso, pertanto il risultato è l'allocazione di memoria aggiuntiva rispetto alle dimensioni dell'intero corpo della richiesta. - L'esempio legge l'intera stringa prima della suddivisione in corrispondenza delle nuove righe. È più efficiente verificare la presenza di nuove righe nella matrice di byte.
Ecco un esempio che risolve alcuni dei problemi precedenti:
Avviso
Il codice seguente:
- Viene usato per illustrare le soluzioni ad alcuni problemi nel codice precedente, senza risolvere tutti i problemi.
- Non deve essere usato nelle app di produzione.
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;
}
Questo esempio precedente:
- Non memorizza nel buffer l'intero corpo della richiesta in un
StringBuilder
, a meno che non siano presenti caratteri di nuova riga. - Non chiama
Split
sulla stringa.
Tuttavia, esistono ancora alcuni problemi:
- Se i caratteri di nuova riga sono di tipo sparse, gran parte del corpo della richiesta viene memorizzata nel buffer nella stringa.
- Il codice continua a creare stringhe (
remainingString
) e le aggiunge al buffer di stringa, che comporta un'allocazione aggiuntiva.
Questi problemi sono risolvibili, ma il codice sta diventando progressivamente più complicato con un piccolo miglioramento. Le pipeline consentono di risolvere questi problemi con complicazioni minime per il codice.
Pipeline
L'esempio seguente illustra come gestire lo stesso scenario usando 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));
}
Questo esempio consente di risolvere molti problemi delle implementazioni dei flussi:
- Non è necessario un buffer di stringa perché gestisce i
PipeReader
byte che non sono stati usati. - Le stringhe codificate vengono aggiunte direttamente all'elenco di stringhe restituite.
- Oltre alla
ToArray
chiamata e alla memoria usata dalla stringa, la creazione di stringhe è libera dall'allocazione.
Adapter
Le Body
proprietà , BodyReader
e BodyWriter
sono disponibili per HttpRequest
e HttpResponse
. Quando si imposta su Body
un flusso diverso, un nuovo set di adattatori si adatta automaticamente a ogni tipo all'altro. Se si imposta su HttpRequest.Body
un nuovo flusso, HttpRequest.BodyReader
viene impostato automaticamente su un nuovo PipeReader
oggetto che esegue il wrapping di HttpRequest.Body
.
StartAsync
HttpResponse.StartAsync
viene usato per indicare che le intestazioni non sono modificabili e per eseguire OnStarting
callback. Quando si usa Kestrel come server, la chiamata StartAsync
prima di usare garantisce che la PipeReader
memoria restituita da GetMemory
appartenga al Kestrelbuffer interno Pipe anziché a un buffer esterno.