Junho de 2019

Volume 34 – Número 6

[Moderno]

Revisitando o pipeline do ASP.NET Core

Por Dino Esposito | Junho de 2019

Dino EspositoPraticamente qualquer ambiente de processamento do servidor tem seu próprio pipeline de componentes de passagem para inspecionar, rotear novamente ou modificar as solicitações de entrada e as respostas de saída. O ASP.NET clássico os organizava na concepção de módulos HTTP, enquanto que o ASP.NET Core utiliza a arquitetura mais moderna, com base nos componentes de middleware. No fim das contas, a finalidade é a mesma: permitir que os módulos externos configuráveis interfiram na maneira como a solicitação (e depois a resposta) passa no ambiente de servidor. O objetivo principal dos componentes de middleware é alterar e filtrar de alguma forma o fluxo de dados (e, em alguns casos específicos, apenas interromper a solicitação, encerrando qualquer processamento adicional).

O pipeline do ASP.NET Core é praticamente o mesmo desde a versão 1.0 do framework, mas o lançamento do ASP.NET Core 3.0 destaca alguns comentários sobre a arquitetura atual, que passaram despercebidos na sua maior parte. Portanto, neste artigo, vou revisitar o funcionamento geral do pipeline de tempo de execução do ASP.NET Core e focar na função e possível implementação dos pontos de extremidade HTTP.

ASP.NET Core para o Back-End da Web

Especialmente nos últimos anos, a criação de aplicativos Web com front-end e back-end completamente dissociados tem se tornado bastante comum. Portanto, a maioria dos projetos do ASP.NET Core de hoje são API da Web simples, projetos sem interface do usuário que fornecem apenas uma fachada HTTP para criação de um aplicativo móvel e/ou de página única, na maior parte, com Angular, React, Vue ou seus equivalentes móveis.

Quando você percebe isso, surge uma pergunta: Em um aplicativo que não está usando nenhum recurso do Razor, ainda faz sentido associar-se ao modelo de aplicativo MVC? O modelo MVC não vem de graça e, na verdade, até certo ponto, pode até não ser a opção mais simples quando você deixa de usar os controladores para transmitir os resultados da ação. Para ir mais longe na pergunta: O conceito de resultados da ação em si é estritamente necessário se um compartilhamento significativo do código do ASP.NET Core é gravado apenas para retornar conteúdo JSON?

Com essas considerações em mente, vamos analisar o pipeline do ASP.NET Core e a estrutura interna dos componentes de middleware, além da lista de serviços de tempo de execução interno que você pode associar durante a inicialização.

A classe Startup

Em qualquer aplicativo ASP.NET Core, uma classe é designada como o bootstrapper do aplicativo. Na maioria das vezes, essa classe usa o nome da inicialização. A classe é declarada como uma classe de inicialização na configuração do host da Web, que instancia e invoca por meio de reflexão. A classe pode ter dois métodos: ConfigureServices (opcional) e Configure. No primeiro método, você recebe a lista atual (padrão) dos serviços de tempo de execução e mais serviços são esperados ser adicionados para preparar o terreno para a lógica real do aplicativo. No método Configure, você configura os serviços padrão além daqueles explicitamente solicitados para dar suporte ao seu aplicativo.

O método Configure recebe pelo menos uma instância da classe de construtor do aplicativo. Você pode ver essa instância como uma instância de trabalho do pipeline de tempo de execução do ASP.NET passada para o código a ser configurado conforme apropriado. Depois de o método Configure ser retornado, o fluxo de trabalho do pipeline estará totalmente configurado e será usado para executar qualquer solicitação adicional enviada de clientes conectados. A Figura 1 fornece um exemplo de implementação do método Configure da classe de inicialização.

Figura 1 Exemplo básico do método Configure na classe de inicialização

public void Configure(IApplicationBuilder app)
{
  app.Use(async (context, nextMiddleware) =>
  {
    await context.Response.WriteAsync("BEFORE");
    await nextMiddleware();  
    await context.Response.WriteAsync("AFTER");
  });
  app.Run(async (context) =>
  {
    var obj = new SomeWork();
    await context
      .Response
      .WriteAsync("<h1 style='color:red;'>" +
                   obj.SomeMethod() +
                  "</h1>");
  });
}

O método Use extension é o principal método a ser usado para adicionar o código de middleware ao fluxo de trabalho de pipeline vazio. Observe que quanto mais middleware você adicionar, mais trabalho o servidor precisará fazer para atender a qualquer solicitação recebida. Quanto menor o pipeline, mais rápido será o tempo-para-o primeiro byte (TTFB) para o cliente.

Você pode adicionar código de middleware ao pipeline usando lambdas ou classes de middleware ad-hoc. A escolha é sua: O lambda é mais direto, mas a classe (e preferencialmente alguns métodos de extensão) facilitará a leitura e manutenção de tudo isso. O código de middleware obtém o contexto HTTP da solicitação e uma referência para o próximo middleware no pipeline, se houver. A Figura 2 apresenta uma visão geral de como vários componentes de middleware associam-se.

O pipeline de tempo de execução do ASP.NET Core
Figura 2 O pipeline de tempo de execução do ASP.NET Core

Cada componente de middleware recebe uma oportunidade dupla para interferir na vida da solicitação em andamento. Ele pode pré-processar a solicitação recebida da cadeia de componentes registrada para executar antecipadamente e, em seguida, espera-se que ele seja entregue ao próximo componente na cadeia. Quando o último componente na cadeia tem a chance de pré-processar a solicitação, a solicitação é passada para o middleware de terminação para o processamento real com o objetivo de gerar uma saída concreta. Depois disso, a cadeia de componentes é percorrida na ordem inversa como mostrado na Figura 2, e cada middleware terá sua segunda chance de processar. Neste momento, no entanto, é uma ação de pós-processamento. No código da Figura 1, a separação entre o código de pré e pós-processamento é a linha:

await nextMiddleware();

O middleware de terminação

O ponto-chave na arquitetura mostrada na Figura 2 é a função de middleware de terminação, que é o código na parte inferior do método Configure que encerra a cadeia e processa a solicitação. Todos os aplicativos ASP.NET Core de demonstração têm uma terminação lambda, assim:

app.Run(async (context) => { ... };

O lambda recebe um objeto HttpContext e faz tudo o que deve fazer no contexto do aplicativo.

Um componente de middleware que deliberadamente não passa adiante, na verdade, ele encerra a cadeia, fazendo com que a resposta seja enviada ao cliente solicitante. Um bom exemplo disso é o middleware de UseStaticFiles, que serve um recurso estático na pasta raiz da Web especificada e encerra a solicitação. Outro exemplo é UseRewriter, que pode ser capaz de solicitar um redirecionamento de cliente para uma nova URL. Sem um middleware de terminação, uma solicitação dificilmente resultará em alguma saída visível no cliente, embora uma resposta ainda seja enviada conforme modificada pelo middleware em execução, por exemplo, por meio da adição de cabeçalhos HTTP ou cookies de resposta.

Há duas ferramentas de middleware dedicado que também podem ser usadas para interromper a solicitação: app.Map e app.MapWhen. A primeira verifica se o caminho da solicitação corresponde ao argumento e executa seu próprio middleware de terminação, conforme mostrado aqui:

app.Map("/now", now =>
{
  now.Run(async context =>
  {
    var time = DateTime.UtcNow.ToString("HH:mm:ss");
    await context
      .Response
      .WriteAsync(time);
  });
});

A segunda, em vez disso, executa seu próprio middleware de terminação somente se uma condição booliana especificada for verificada. A condição booliana resulta da avaliação de uma função que aceita um HttpContext. O código na Figura 3 apresenta uma API da Web muito fina e básica que só serve um único ponto de extremidade e faz isso sem nada como uma classe de controlador.

Figura 3 Uma API da Web muito básica do ASP.NET Core

public void Configure(IApplicationBuilder app,
                      ICountryRepository country)
{
  app.Map("/country", countryApp =>
  {
    countryApp.Run(async (context) =>
    {
      var query = context.Request.Query["q"];
      var list = country.AllBy(query).ToList();
      var json = JsonConvert.SerializeObject(list);
      await context.Response.WriteAsync(json);
    });
  });
  // Work as a catch-all
  app.Run(async (context) =>
  {
    await context.Response.WriteAsync("Invalid call");
  }
});

Se a URL corresponder /country, o middleware de terminação lê um parâmetro de cadeia de caracteres de consulta e organiza uma chamada pelo repositório para obter a lista de países correspondentes. O objeto de lista é então serializado manualmente em um formato JSON diretamente para o fluxo de saída. Adicionando apenas algumas outras rotas de mapa você poderia até mesmo estender sua API da Web. Dificilmente pode ser mais simples que isso.

E quanto ao MVC?

No ASP.NET Core, o mecanismo do MVC inteiro é oferecido como um serviço de tempo de execução de caixa preta. Tudo que você faz é associar-se ao serviço no método ConfigureServices e configurar suas rotas no método Configure, conforme mostrado no código aqui:

public void ConfigureServices(IServiceCollection services)
{
  // Bind to the MVC machinery
  services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
  // Use the MVC machinery with default route
  app.UseMvcWithDefaultRoute();
  // (As an alternative) Use the MVC machinery with attribute routing
  app.UseMvc();
}

Nesse ponto, você pode preencher a pasta familiar Controladores e até mesmo a pasta Exibições, caso pretenda fornecer HTML. Observe que no ASP.NET Core você também pode usar controladores POCO, que são classes C# simples decoradas para serem reconhecidas como controladores e desconectadas do contexto HTTP.

O mecanismo do MVC é outro ótimo exemplo de middleware de terminação. Depois que a solicitação é capturada pelo middleware do MVC, tudo fica sob o controle dele e o pipeline é encerrado abruptamente.

É interessante notar que internamente o mecanismo MVC executa seu próprio pipeline personalizado. Ele não é centrado em middleware, mas ainda assim é um pipeline de tempo de execução completo que controla como as solicitações são roteadas para o método de ação do controlador, com o resultado de ação gerado, por fim, processado no fluxo de saída. O pipeline MVC é composto de vários tipos de filtros de ação (seletores de nome de ação, filtros de autorização, manipuladores de exceção, gerentes de resultado de ação personalizada) que são executados antes e depois de cada método de controlador. No ASP.NET Core, a negociação de conteúdo também está incluída no pipeline de tempo de execução.

Dentro de uma visão mais criteriosa, todo o mecanismo de MVC do ASP.NET parece estar ligado ao mais recente e reprojetado pipeline centrado em middleware do ASP.NET Core. É como se o pipeline do ASP.NET Core e o mecanismo de MVC fossem entidades de diferentes tipos apenas conectadas de alguma forma. A visão geral não é muito diferente da maneira como o MVC foi colocado no topo do tempo de execução do Web Forms ignorado por enquanto. Nesse contexto, na verdade, o MVC foi iniciado por meio de um manipulador HTTP dedicado se a solicitação de processamento não pôde ser correspondida em um arquivo físico (provavelmente um arquivo ASPX).

Isso é um problema? Provavelmente não. Ou provavelmente ainda não.

Colocando o SignalR no Loop

Quando você adiciona o SignalR a um aplicativo ASP.NET Core, tudo o que você precisa fazer é criar uma classe de hub para expor seus pontos de extremidade. O interessante é que a classe hub pode ser completamente independente dos controladores. Você não precisa do MVC para executar o SignalR, ainda que a classe hub se comporte como um controlador de front-end para as solicitações externas. Um método exposto de uma classe hub pode executar qualquer trabalho, até mesmo não relacionado à natureza entre aplicativos de notificação do framework, conforme mostrado na Figura 4.

Figura 4 Expondo um método de uma classe de Hub

public class SomeHub : Hub
{
  public void Method1()
  {
    // Some logic
    ...
    Clients.All.SendAsync("...");
  }
  public void Method2()
  {
    // Some other logic
    ...
    Clients.All.SendAsync("...");
  }
}

Consegue ver a imagem?

A classe do SignalR hub pode ser vista como uma classe de controlador, sem o mecanismo inteiro de MVC, ideal para respostas sem interface do usuário (ou, em vez disso, sem Razor).

Colocando gRPC no Loop

Na versão 3.0, o ASP.NET Core também fornece suporte nativo para o framework gRPC. Desenvolvida junto com as diretrizes RPC, a estrutura é um shell de código em torno de uma linguagem de definição de interface que define o ponto de extremidade totalmente e é capaz de acionar a comunicação entre as partes conectadas usando a serialização binária Protobuf em HTTP/2. Da perspectiva do ASP.NET Core 3.0, gRPC é ainda outra fachada invocável que pode fazer cálculos do lado do servidor e retornar valores. Aqui está como habilitar um aplicativo de servidor do ASP.NET Core para dar suporte a gRPC:

public void ConfigureServices(IServiceCollection services)
{
  services.AddGrpc();
}
public void Configure(IApplicationBuilder app)
{
  app.UseRouting(routes =>
    {
      routes.MapGrpcService<GreeterService>();
    });
}

Observe também o uso de roteamento global para permitir que o aplicativo dê suporte a rotas sem o mecanismo MVC. Você pode pensar em UseRouting como uma maneira mais estruturada de definição dos blocos de middleware de app.Map.

O efeito líquido do código anterior é habilitar chamadas no estilo RPC de um aplicativo cliente para o serviço mapeado, a classe GreeterService. O interessante é que a classe GreeterService é conceitualmente equivalente a um controlador POCO, exceto que ela não precisa ser reconhecida como uma classe de controlador, como mostrado aqui:

public class GreeterService : Greeter.GreeterBase
{
  public GreeterService(ILogger<GreeterService> logger)
  {
  }
}

A classe base (GreeterBase é uma classe abstrata encapsulada em uma classe estática) contém os detalhes técnicos necessários para executar o tráfego de solicitação/resposta. A classe de serviço gRPC é totalmente integrada com a infraestrutura do ASP.NET Core e pode ser injetada com referências externas.

Conclusão

Especialmente com o lançamento do ASP.NET Core 3.0, haverá dois outros cenários em que ter uma fachada de estilo de controlador sem MVC seria útil. O SignalR tem classes de hub e gRPC tem uma classe de serviço, mas o ponto é que eles conceitualmente são a mesma coisa que precisa ser implementada de maneiras diferentes para diversos cenários. O mecanismo do MVC foi movido para o ASP.NET Core mais ou menos à medida em que ele foi originalmente desenvolvido para o ASP.NET clássico, e mantém seu próprio pipeline interno em torno de controladores e resultados de ação. Ao mesmo tempo, como o ASP.NET Core é cada vez mais usado como um provedor simples dos serviços de back-end, sem suporte para modos de exibição, aumenta a necessidade de uma fachada de estilo RPC, possivelmente, unificada, para pontos de extremidade HTTP.


Dino Esposito é autor de mais de 20 livros e de mais de mil artigos em seus 25 anos de carreira. Autor de “The Sabbatical Break”, um show de estilo cênico, Esposito se ocupa escrevendo software para um mundo mais ecológico, como estrategista digital da BaxEnergy. Siga-o no Twitter: @despos.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Marco Cecconi


Discuta esse artigo no fórum do MSDN Magazine