Opérations de demande et de réponse dans ASP.NET Core
Remarque
Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 9 de cet article.
Avertissement
Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la stratégie de support .NET et .NET Core. Pour la version actuelle, consultez la version .NET 9 de cet article.
Important
Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.
Pour la version actuelle, consultez la version .NET 9 de cet article.
Par Justin Kotalik
Cet article explique comment lire à partir du corps de la demande et écrire dans le corps de la réponse. Le code pour ces opérations peut être requis lors de l’écriture d’intergiciels. En dehors de l’écriture d’intergiciels, le code personnalisé n’est généralement pas requis, car les opérations sont gérées par MVC et Razor Pages.
Il existe deux abstractions pour les corps de la requête et de la réponse : Stream et Pipe. Pour la lecture des requêtes, HttpRequest.Body est un Streamet HttpRequest.BodyReader
est un PipeReader. Pour l’écriture de réponses, HttpResponse.Body est un Streamet HttpResponse.BodyWriter
est un PipeWriter.
Les pipelines sont recommandés sur les flux. Les flux peuvent être plus faciles à utiliser pour des opérations simples, mais les pipelines présentent un avantage de performances et sont plus faciles à utiliser dans la plupart des scénarios. ASP.NET Core commence à utiliser des pipelines au lieu de flux en interne. Voici quelques exemples :
FormReader
TextReader
TextWriter
HttpResponse.WriteAsync
Les flux ne sont pas supprimés de l’infrastructure. Les flux continuent à être utilisés dans .NET et de nombreux types de flux n’ont d’équivalents en termes de canal, comme FileStreams
et ResponseCompression
.
Exemples de flux
Supposons que votre objectif est de créer un intergiciel qui lit le corps de la requête dans sa totalité comme une liste de chaînes, avec un fractionnement sur les nouvelles lignes. Une implémentation simple de flux peut se présenter comme dans l’exemple suivant :
Avertissement
Le code suivant :
- Est utilisé pour illustrer les problèmes liés à l’utilisation d’un canal pour lire le corps de la requête.
- N’est pas destiné à être utilisé dans les applications de production.
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"));
}
Si vous souhaitez voir les commentaires de code traduits dans une langue autre que l’anglais, dites-le nous dans cette discussion GitHub.
Ce code fonctionne, mais il existe certains problèmes :
- Avant d’ajouter à
StringBuilder
, l’exemple crée une autre chaîne (encodedString
) qui est immédiatement rejetée. Ce processus se produit pour tous les octets dans le flux, il en résulte une allocation de mémoire supplémentaire de la taille de la totalité du corps de la demande. - L’exemple lit la chaîne entière avant de fractionner sur les nouvelles lignes. Il est plus efficace de rechercher les nouvelles lignes dans le tableau d’octets.
Voici un exemple qui résout certains des problèmes précédents :
Avertissement
Le code suivant :
- Est utilisé pour illustrer les solutions à certains problèmes dans le code précédent sans résoudre tous les problèmes.
- N’est pas destiné à être utilisé dans les applications de production.
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;
}
Cet exemple précédent :
- Ne met pas en mémoire tampon le corps entier de la demande dans un
StringBuilder
, sauf s’il n’y a pas de caractère de nouvelle ligne. - N’appelle pas
Split
sur la chaîne.
Toutefois, il existe toujours quelques problèmes :
- Si les caractères nouvelle ligne sont épars, une grande partie du corps de la requête est mis en mémoire tampon dans la chaîne.
- Le code continue de créer des chaînes (
remainingString
) et les ajoute à la mémoire tampon de chaîne, ce qui entraîne une allocation supplémentaire.
Ces problèmes sont réparables, mais le code est de plus en plus compliqué avec peu d’amélioration. Les pipelines permettent de résoudre ces problèmes avec un code peu compliqué.
Pipelines
L’exemple suivant indique comment le même scénario peut être géré à l’aide d’un 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));
}
Cet exemple résout de nombreux problèmes trouvés dans les implémentations de flux :
- Une mémoire tampon de chaîne est inutile, car le
PipeReader
gère des octets qui n’ont pas été utilisés. - Les chaînes codées sont ajoutées directement à la liste des chaînes retournées.
- À l’exception de l’appel
ToArray
, et de la mémoire utilisée par la chaîne, la création de chaînes est libre d’allocation.
Adaptateurs
Les propriétés Body
, BodyReader
et BodyWriter
sont disponibles pour HttpRequest
et HttpResponse
. Lorsque vous définissez Body
sur un autre flux, un nouvel ensemble d’adaptateurs adapte automatiquement chaque type à l’autre. Si vous définissez HttpRequest.Body
sur un nouveau flux, HttpRequest.BodyReader
est automatiquement défini sur un nouveau PipeReader
qui enveloppe HttpRequest.Body
.
StartAsync
HttpResponse.StartAsync
est utilisé pour indiquer que les en-têtes sont non modifiables et pour exécuter des rappels OnStarting
. Lors de l’utilisation de Kestrel en tant que serveur, l’appel de StartAsync
avant d’utiliser le PipeReader
garantit que la mémoire retournée par GetMemory
appartiendra au Pipe interne de Kestrel au lieu d’une mémoire tampon externe.