Compartilhar via


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 a versão atual, consulte a versão do .NET 10 deste artigo.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. 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 do .NET 10 deste artigo.

Por Justin Kotalik

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.

Os 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. Os fluxos continuam a ser usados em todo o .NET:

  • Muitos tipos de stream não têm equivalentes de pipe, como FileStreams e ResponseCompression.
  • É simples adicionar compactação a um fluxo.

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 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 cenário de fluxo anterior 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.

Ao gravar diretamente em HttpResponse.BodyWriter, chame PipeWriter.FlushAsync manualmente para garantir que os dados sejam enviados para o corpo da resposta subjacente. Veja o motivo:

  • HttpResponse.BodyWriter é um PipeWriter que armazena dados temporariamente até que uma operação de descarga seja disparada.
  • A chamada FlushAsync grava os dados armazenados em buffer no corpo da resposta subjacente.

Cabe ao desenvolvedor decidir quando chamar FlushAsync, balanceando fatores 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 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 Kestrel interna do Pipe em vez de a um buffer externo.

Recursos adicionais