Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Observação
Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 10 deste artigo.
Advertência
Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.
Por Justin Kotalik
Este artigo explica como ler o corpo da solicitação e escrever no corpo da resposta. O código para essas operações pode ser necessário ao escrever middleware. Exceto quando se escreve middleware, o código personalizado geralmente não é necessário porque as operações são tratadas pelo MVC e pelas Pages.
Há duas abstrações para os órgãos de solicitação e resposta: Stream e Pipe. Para solicitação de leitura, HttpRequest.Body é um Stream, e HttpRequest.BodyReader é um PipeReader. Para a escrita de respostas, HttpResponse.Body é um Stream, e HttpResponse.BodyWriter é um PipeWriter.
Dutos são recomendados sobre fluxos. Os fluxos podem ser mais fáceis de usar para algumas operações simples, mas os pipelines têm uma vantagem de desempenho e são mais fáceis de usar na maioria dos cenários. ASP.NET Core está começando a usar pipelines em vez de fluxos internamente. Os exemplos incluem:
FormReaderTextReaderTextWriterHttpResponse.WriteAsync
Os fluxos não estão sendo removidos da estrutura. Os fluxos continuam a ser usados em todo o .NET:
- Muitos tipos de fluxo não têm equivalentes de tubo, como
FileStreamseResponseCompression. - É simples adicionar compressão a um fluxo.
Exemplos de fluxo
Suponha que o objetivo seja criar um middleware que leia todo o corpo da solicitação como uma lista de cadeias de caracteres, dividindo em novas linhas. Uma implementação de fluxo simples pode se parecer com o exemplo a seguir:
Advertência
O seguinte código:
- É usado para demonstrar os problemas com o não uso de um tubo para ler o corpo da solicitação.
- Não se destina a ser usado em aplicativos de produção.
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"));
}
Se quiser ver os comentários de código traduzidos para outros idiomas além do inglês, avise-nos nesta discussão do GitHub .
Este código funciona, mas há alguns problemas:
- Antes de acrescentar ao
StringBuilder, o exemplo cria outra cadeia de caracteres (encodedString) que é descartada imediatamente. Esse processo ocorre para todos os bytes no fluxo, portanto, o resultado é a alocação de memória extra do tamanho de todo o corpo da solicitação. - O exemplo lê toda a cadeia de caracteres antes de dividir em novas linhas. É mais eficiente verificar se há novas linhas na matriz de bytes.
Aqui está um exemplo que corrige alguns dos problemas anteriores:
Advertência
O seguinte código:
- É usado para demonstrar as soluções para alguns problemas no código anterior enquanto não resolve todos os problemas.
- Não se destina a ser usado em aplicativos de produção.
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;
}
Este exemplo anterior:
- Não armazena em buffer todo o corpo da solicitação em um
StringBuilder, a menos que não haja caracteres de nova linha. - Não chama
Splitpara a string.
No entanto, subsistem ainda algumas questões:
- Se os caracteres da nova linha forem esparsos, grande parte do corpo da solicitação será armazenada em buffer na cadeia de caracteres.
- O código continua a criar cadeias de caracteres (
remainingString) e as adiciona ao buffer de cadeia de caracteres, o que resulta em uma alocação extra.
Esses problemas são corrigíveis, mas o código está se tornando progressivamente mais complicado com poucas melhorias. Os pipelines fornecem uma maneira de resolver esses problemas com o mínimo de complexidade de código.
Tubulações
O exemplo a seguir mostra como o cenário de fluxo anterior pode ser manipulado usando um 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));
}
Este exemplo corrige muitos problemas que as implementações de fluxos tinham:
- Não há necessidade de um buffer de cadeia de caracteres porque o
PipeReadermanipula bytes que não foram usados. - As cadeias de caracteres codificadas são adicionadas diretamente à lista de cadeias de caracteres retornadas.
- Além da
ToArraychamada e da memória usada pela cadeia de caracteres, a criação da cadeia de caracteres é livre de alocação.
Ao gravar diretamente no HttpResponse.BodyWriter, chame PipeWriter.FlushAsync manualmente para garantir que os dados sejam liberados para o corpo de resposta subjacente. Aqui está o porquê:
-
HttpResponse.BodyWriteré umPipeWriterque armazena dados em buffer até que uma operação de liberação seja acionada. - A chamada
FlushAsyncgrava os dados armazenados em buffer no corpo de resposta subjacente.
Cabe ao desenvolvedor decidir quando chamar FlushAsync, fatores de balanceamento, como tamanho do buffer, sobrecarga de gravação de rede e se os dados devem ser enviados em partes discretas. Para obter mais informações, consulte System.IO.Pipelines no .NET.
Adaptadores
As Bodypropriedades , BodyReader, e BodyWriter estão disponíveis para HttpRequest e HttpResponse. Quando você define Body para um fluxo diferente, um novo conjunto de adaptadores adapta automaticamente cada tipo ao outro. Se você definir HttpRequest.Body como um novo fluxo, HttpRequest.BodyReader será automaticamente definido como um novo PipeReader que encapsula HttpRequest.Body.
StartAsync
HttpResponse.StartAsync é usado para indicar que os cabeçalhos são imutáveis e para executar OnStarting callbacks. Ao usar Kestrel como um servidor, chamar StartAsync antes de usar PipeReader garante que a memória retornada por GetMemory pertence ao Kestrel interno de Pipe em vez de a um buffer externo.
Recursos adicionais
- System.IO.Pipelines em .NET
- Escrever um middleware personalizado para ASP.NET Core