Operações de solicitação e de resposta no ASP.NET Core
Observação
Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Aviso
Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Importante
Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.
Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Este artigo explica como ler o corpo da solicitação e gravar no corpo da resposta. O código para essas operações pode ser necessário ao escrever middleware. Fora da gravação de middleware, o código personalizado geralmente não é necessário porque as operações são tratadas pelo MVC e pelo Razor Pages.
Há duas abstrações para os corpos de solicitação e resposta: Stream e Pipe. Para leitura de solicitação, HttpRequest.Body é um Stream e HttpRequest.BodyReader
é um PipeReader. Para escrever respostas, HttpResponse.Body é um Stream e HttpResponse.BodyWriter
é um PipeWriter.
Pipelines são recomendados em fluxos. Os fluxos podem ser mais fáceis de usar em algumas operações simples, mas os pipelines têm uma vantagem no desempenho e são mais fáceis de usar na maioria dos cenários. O ASP.NET Core começa a usar pipelines em vez de fluxos internamente. Os exemplos incluem:
FormReader
TextReader
TextWriter
HttpResponse.WriteAsync
Os fluxos não estão sendo removidos da estrutura. Eles continuarão a ser usados em todo o .NET e muitos tipos de fluxo não possuem equivalentes no pipeline, como FileStreams
e ResponseCompression
.
Exemplos de fluxos
Suponha que o objetivo seja criar um middleware para ler todo o corpo da solicitação como uma lista de cadeias de caracteres, dividindo em novas linhas. Uma implementação simples de fluxo pode ter uma aparência semelhante ao exemplo a seguir:
Aviso
O seguinte código:
- É usado para demonstrar os problemas de não usar um pipe para ler o corpo da solicitação.
- Não se destina ao uso 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 você quiser ver os comentários de código traduzidos para idiomas diferentes do inglês, informe-nos neste problema de discussão do GitHub.
Esse código funciona, mas há alguns problemas:
- Antes de ser acrescentado ao
StringBuilder
, o exemplo cria outra cadeia de caracteres (encodedString
), que é imediatamente descartada. Esse processo ocorre em todos os bytes no fluxo, portanto, o resultado é uma alocação extra de memória do tamanho de todo o corpo da solicitação. - O exemplo lê a cadeia de caracteres inteira antes da divisão em novas linhas. É mais eficiente verificar se há novas linhas na matriz de bytes.
Veja um exemplo que corrige alguns dos problemas anteriores:
Aviso
O seguinte código:
- É usado para demonstrar as soluções para alguns problemas no código anterior, sem resolver todos os problemas.
- Não se destina ao uso 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;
}
Esse exemplo anterior:
- Não armazena em buffer todo o corpo da solicitação em um
StringBuilder
, a menos que não haja caracteres da nova linha. - Não chama
Split
na cadeia de caracteres.
No entanto, ainda há alguns problemas:
- Se os caracteres de nova linha forem esparsos, grande parte do corpo da solicitação será armazenado em buffer na cadeia de caracteres.
- O código continua a criar cadeias de caracteres (
remainingString
) e as adiciona ao buffer da cadeia de caracteres, o que resultará em uma alocação extra.
Esses problemas são corrigíveis, mas o código está se tornando progressivamente mais complicado com pouca melhoria. Os pipelines oferecem uma maneira de resolver esses problemas com uma complexidade de código mínima.
Pipelines
O exemplo a seguir mostra como o mesmo cenário pode ser tratado 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 das implementações de fluxos:
- Um buffer de cadeia de caracteres não é necessário porque o
PipeReader
lida com bytes que não foram usados. - As cadeias de caracteres codificadas são adicionadas diretamente à lista de cadeias de caracteres retornadas.
- Além da chamada
ToArray
e da memória usada pela cadeia de caracteres, a criação de cadeia de caracteres é livre de alocação.
Adaptadores
As propriedades Body
, BodyReader
e BodyWriter
estão disponíveis para HttpRequest
e HttpResponse
. Quando você define Body
como um fluxo diferente, um novo conjunto de adaptadores adapta automaticamente cada tipo para o 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 não poderão ser modificados e para executar retornos de chamada OnStarting
. Ao usar o Kestrel como servidor, chamar StartAsync
antes de usar o PipeReader
garante que a memória retornada por GetMemory
pertença a Pipe interna do Kestrel em vez de a um buffer externo.