Junio de 2019

Volumen 34, número 6

[Tecnología de vanguardia]

Revisión de la canalización de ASP.NET Core

Por Dino Esposito | Junio de 2019

Dino EspositoPrácticamente cualquier entorno de procesamiento del lado servidor tiene su propia canalización de componentes de paso a través para inspeccionar, volver a enrutar o modificar las solicitudes entrantes y las respuestas salientes. ASP.NET clásico lo tenía organizado en torno a la idea de los módulos HTTP, mientras que ASP.NET Core emplea la arquitectura más moderna basada en componentes de middleware. Al final del día, el propósito es el mismo: permitir que los módulos externos configurables intervengan en la forma en que la solicitud (y posteriormente, la respuesta) pasa por el entorno de servidor. El propósito principal de los componentes de middleware es modificar, y filtrar de alguna manera, el flujo de datos (y en algunos casos interrumpir la solicitud y detener así cualquier procesamiento posterior).

La canalización de ASP.NET Core se mantiene prácticamente sin cambios desde la versión 1.0 del marco, pero la próxima versión de ASP.NET Core 3.0 invita algunos comentarios sobre la arquitectura actual, que han pasado desapercibidos en gran parte. Por lo tanto, en este artículo, repasaré el funcionamiento general de la canalización en tiempo de ejecución de ASP.NET Core, y me centraré en el rol y la posible implementación de puntos de conexión HTTP.

ASP.NET Core para el back-end web

Especialmente en los últimos dos años, es bastante común la compilación de aplicaciones web con el front-end y el back-end completamente desacoplados. Por lo tanto, la mayoría de los proyectos de ASP.NET Core actualmente son proyectos de API web sin UI que simplemente proporcionan una fachada HTTP para una página única o una aplicación móvil compilada, en su mayor parte, con Angular, React, Vue y sus homólogos móviles.

Cuando se de cuenta de ello, le surgirá una pregunta: En una aplicación que no utiliza ninguna prestación de Razor, ¿sigue teniendo sentido establecer un vínculo al modelo de aplicaciones de MVC? El modelo de MVC no es gratuito y, en cierta medida, es posible que no sea la opción más ligera una vez que deje de usar los controladores para mostrar los resultados de la acción. Para complicar aún más la pregunta: ¿El concepto de resultado de la acción en sí es estrictamente necesario si una parte significativa del código de ASP.NET Core está escrito solo para devolver cargas útiles de JSON?

Con estas ideas en mente, vamos a revisar la canalización de ASP.NET Core y la estructura interna de los componentes de middleware , así como la lista de servicios en tiempo de ejecución integrados con los que puede establecer un vínculo durante el inicio.

La clase Startup

En cualquier aplicación de ASP.NET Core, se designa una clase como programa previo de la aplicación. La mayoría de las veces, esta clase toma el nombre de Startup. La clase se declara como una clase Startup en la configuración del host web, y el host web crea la instancia y la invoca a través de la reflexión. La clase puede tener dos métodos: ConfigureServices (opcional) y Configure. En el primer método, recibe la lista de servicios en tiempo en ejecución (predeterminada) actual y se espera que agregue más servicios para preparar el terreno para la lógica de la aplicación real. En el método Configure, debe realizar la configuración de los servicios predeterminados y de los que solicite explícitamente para la compatibilidad de su aplicación.

El método Configure recibe al menos una instancia de la clase de generador de aplicaciones. Puede ver esta instancia como una instancia de trabajo de la canalización en tiempo de ejecución de ASP.NET pasada al código para su debida configuración. Tras la devolución del método Configure, el flujo de trabajo de la canalización estará completamente configurado y se usará para realizar cualquier solicitud posterior que se envíe desde los clientes conectados. La Figura 1 proporciona una implementación de ejemplo del método Configure de una clase Startup.

Figura 1 Ejemplo básico del método Configure de la clase Startup

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>");
  });
}

El método de extensión Use es el método principal que usa para agregar código de middleware al flujo de trabajo de canalización que, de otro modo, estaría vacío. Tenga en cuenta que cuanto más middleware agregue, más trabajo deberá realizar el servidor para atender las solicitudes entrantes. Cuanto menor sea la canalización, más rápido será el tiempo hasta el primer byte (TTFB) para el cliente.

Puede agregar código de middleware a la canalización mediante expresiones lambda o clases de middleware ad hoc. La decisión es suya: La expresión lambda es más directa, pero la clase (y preferiblemente algunos métodos de extensión) hará que todo sea más fácil de leer y mantener. El código de middleware obtiene el contexto HTTP de la solicitud y una referencia al siguiente middleware de la canalización, si existe. La Figura 2 presenta una vista general de la vinculación entre los distintos componentes de middleware.

Canalización en tiempo de ejecución de ASP.NET Core
Figura 2 Canalización en tiempo de ejecución de ASP.NET Core

Cada componente de middleware tiene una doble oportunidad de intervenir en el ciclo de la solicitud en curso. Puede preprocesar la solicitud como procedente de la cadena de componentes registrada para ejecutarse anteriormente y, a continuación, se espera que pase al componente siguiente de la cadena. Cuando el último componente de la cadena obtiene su oportunidad de preprocesar la solicitud, la solicitud se pasa al middleware terminador para el procesamiento real destinado a generar una salida concreta. Posteriormente, se recorre la cadena de componentes en el orden inverso, como se muestra en la Figura 2, y cada middleware tiene su segunda oportunidad de procesamiento, aunque esta vez será una acción de posprocesamiento. En el código de la Figura 1, la separación entre el código de preprocesamiento y posprocesamiento es la línea:

await nextMiddleware();

Middleware terminador

La clave de la arquitectura que se muestra en la Figura 2 es el rol del middleware terminador, que es el código en la parte inferior del método Configure que finaliza la cadena y procesa la solicitud. Todas las aplicaciones de ASP.NET Core tienen una expresión lambda de terminación, como se muestra a continuación:

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

La expresión lambda recibe un objeto HttpContext y hace lo que se supone que debe hacer en el contexto de la aplicación.

Un componente de middleware que no pasa deliberadamente al siguiente finaliza realmente la cadena, lo que provoca que la respuesta se envíe al cliente solicitante. Un buen ejemplo de esto es el middleware UseStaticFiles, que sirve un recurso estático en la carpeta raíz web especificada y finaliza la solicitud. Otro ejemplo es UseRewriter, que puede solicitar un redireccionamiento de cliente a una nueva dirección URL. Sin un middleware terminador, una solicitud apenas puede producir alguna salida visible en el cliente, aunque de todos modos se envía una respuesta modificada mediante la ejecución del middleware, por ejemplo mediante la adición de encabezados HTTP o cookies de respuesta.

Existen dos herramientas de middleware dedicadas que también se pueden usar para interrumpir la solicitud: app.Map y app.MapWhen. La primera comprueba si la ruta de acceso de la solicitud coincide con el argumento y ejecuta su propio middleware terminador, como se muestra aquí:

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

La segunda, en cambio, ejecuta su propio middleware terminador solo si se verifica una condición booleana especificada. La condición booleana procede de la evaluación de una función que acepta un método HttpContext. El código de la Figura 3 presenta una API web muy ligera y minimalista que solo sirve un único punto de conexión sin usar ningún tipo de clase de controlador.

Figura 3 API web de ASP.NET Core muy minimalista

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");
  }
});

Si la dirección URL coincide con /country, el middleware terminador lee un parámetro de la cadena de consulta y organiza una llamada al repositorio para obtener la lista de países coincidentes. A continuación, el objeto list se serializa manualmente en un formato JSON directamente en el flujo de salida. Solo agregando algunas otras rutas de mapa incluso podría ampliar su API web. No puede ser más sencillo.

¿Qué hay de MVC?

En ASP.NET Core, la maquinaria de MVC completa se ofrece como un servicio en tiempo de ejecución de caja negra. Todo lo que debe hacer es definir un enlace al servicio en el método ConfigureServices y configurar sus rutas en el método Configure, tal como se muestra en el código siguiente:

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();
}

En este momento, puede rellenar la carpeta Controllers conocida e incluso la carpeta Views si pretende servir HTML. Observe que en ASP.NET Core también puede usar los controladores POCO, que son clases de C# sin formato decoradas para reconocerse como controladores y desconectadas del contexto HTTP.

La maquinaria de MVC es otro buen ejemplo de middleware terminador. Una vez que el middleware de MVC haya capturado la solicitud, todo queda bajo su control y la canalización finaliza precipitadamente.

Es interesante observar que, internamente, la maquinaria de MVC ejecuta su propia canalización personalizada. No está centrada en el middleware, pero, sin embargo, es una canalización en tiempo de ejecución completa que controla cómo se enrutan las solicitudes al método de acción del controlador, con el resultado de la acción generado que finalmente se representa en el flujo de salida. La canalización de MVC está compuesta de varios tipos de filtros de acción (selectores de nombre de acción, filtros de autorización, controladores de excepciones, administradores de resultados de acción personalizada), que se ejecutan antes y después de cada método controller. En ASP.NET Core, la negociación de contenido también se oculta en la canalización de tiempo de ejecución.

Si observamos con más detenimiento, la maquinaria de MVC de ASP.NET parece estar anclada en la canalización basada en middleware más reciente y rediseñada de ASP.NET Core. Parece que la canalización de ASP.NET Core y la maquinaria de MVC son entidades de distintos tipos que acaban de conectarse entre sí de alguna manera. La imagen general no es muy diferente de la manera en que MVC estaba anclado en el tiempo de ejecución tiempo de ejecución de formularios Web Forms, ahora descartardo. En ese contexto, de hecho, MVC entraba en acción mediante un controlador HTTP dedicado si la solicitud de procesamiento no se podía relacionar con un archivo físico (probablemente un archivo ASPX).

¿Es esto un problema? Probablemente no. O, probablemente, no por ahora.

Colocación de SignalR en el bucle

Al agregar SignalR a una aplicación de ASP.NET Core, todo lo que debe hacer es crear una clase hub para exponer sus puntos de conexión. Lo interesante es que la clase hub puede no guardar ninguna relación en absoluto con los controladores. No es necesario MVC para ejecutar SignalR, aunque la clase hub se comporta como un controlador de front-end para las solicitudes externas. Un método expuesto de una clase hub puede realizar cualquier trabajo, aunque no esté relacionado con la naturaleza de notificación entre aplicaciones del marco, como se muestra en la Figura 4.

Figura 4 Exposición de un método de una clase Hub

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

¿Puede ver la imagen?

La clase hub de SignalR se puede considerar una clase controller, sin la maquinaria de MVC completa, ideal para respuestas sin UI (o, en su lugar, sin Razor).

Colocación de gRPC en el bucle

En la versión 3.0, ASP.NET Core también proporciona compatibilidad nativa para el marco de gRPC. Diseñado junto con las directrices de RPC, el marco es un shell de código alrededor de un lenguaje de definición de interfaz, que define el punto de conexión totalmente y es capaz de desencadenar la comunicación entre las partes conectadas mediante la serialización binaria de Protobuf a través de HTTP/2. Desde la perspectiva de ASP.NET Core 3.0, gRPC es otra fachada invocable que puede realizar cálculos del lado servidor y devolver valores. Aquí se muestra cómo habilitar una aplicación de servidor de ASP.NET Core para que admita gRPC:

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

Tenga en cuenta también el uso del enrutamiento global para habilitar la aplicación con el fin de que admita rutas sin la maquinaria de MVC. Se puede considerar UseRouting como una manera más estructurada de definir los bloques de middleware app.Map.

El efecto neto del código anterior es habilitar las llamadas de estilo RPC desde una aplicación cliente en el servicio asignado, la clase GreeterService. Curiosamente, la clase GreeterService es conceptualmente equivalente a un controlador POCO, excepto en que no tiene que reconocerse como una clase controller, como se muestra a continuación:

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

La clase base (GreeterBase es una clase abstracta que se encapsula en una clase estática) contiene los mecanismos necesarios para llevar a cabo el tráfico de solicitud/respuesta. La clase de servicio gRPC está totalmente integrada en la infraestructura de ASP.NET Core y permite la inserción de referencias externas.

En resumen

Especialmente con el lanzamiento de ASP.NET Core 3.0, habrá dos escenarios más en los que resultará útil tener una fachada con estilo de controlador sin MVC. SignalR tiene clases hub y gRPC tiene una clase service, pero lo importante es que son conceptualmente lo mismo, pero deben implementarse de maneras diferentes para diferentes escenarios. La maquinaria de MVC se ha portado a ASP.NET Core más o menos como estaba previsto inicialmente para ASP.NET clásico. Además, mantiene su propia canalización interna en torno a los controladores y los resultados de acción. Al mismo tiempo, dado que ASP.NET Core se utiliza cada vez más como un proveedor estándar de servicios de back-end, sin compatibilidad para las vistas, crece la necesidad de una fachada de estilo RPC, posiblemente unificada, para los puntos de conexión HTTP.


Dino Esposito ha escrito más de 20 libros y más de 1000 artículos en su carrera de 25 años. Autor de “The Sabbatical Break”, un espectáculo de estilo teatral, Esposito se dedica a escribir software para un mundo más verde como estratega digital de BaxEnergy. Puede seguirle en Twitter: @despos.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Marco Cecconi


Comente este artículo en el foro de MSDN Magazine