Abril de 2018

Volumen 33, número 4

Cutting Edge: detección de ASP.NET Core SignalR

Por Dino Esposito | Abril de 2018

Dino EspositoASP.NET SignalR se presentó hace algunos años como una herramienta para desarrolladores de ASP.NET, para agregar funcionalidad en tiempo real a las aplicaciones. Cualquier escenario en que una aplicación basada en ASP.NET deba recibir actualizaciones frecuentes y asíncronas del servidor (desde sistemas de supervisión a juegos) era un buen caso práctico para la biblioteca. A lo largo de los años, la usé para actualizar la UI en escenarios de arquitectura de CQRS y para implementar un sistema de notificaciones similar al de Facebook en aplicaciones sociales. Desde una perspectiva más técnica, SignalR es una capa de abstracción creada sobre algunos mecanismos de transporte que pueden establecer una conexión en tiempo real entre un cliente y un servidor completamente compatibles. El cliente suele ser un explorador web y el servidor suele ser un servidor web, pero no se limitan a eso.

ASP.NET SignalR forma parte de ASP.NET Core 2.1. El modelo de programación general de la biblioteca es similar al de ASP.NET clásico, pero la propia biblioteca se ha reescrito por completo. Aún así, los desarrolladores deberían sentirse cómodos rápidamente con el nuevo esquema, una vez ajustados a los cambios que se producen aquí y allí. En este artículo, explicaré cómo usar la nueva biblioteca en una aplicación web canónica para supervisar una tarea remota y, probablemente, larga.

Configuración del entorno

Puede que necesite un par de paquetes de NuGet para usar la biblioteca: Microsoft.AspNetCore.SignalR y Microsoft.AspNetCore.SignalR.Client. El primero proporciona la funcionalidad principal; el segundo paquete es el cliente de .NET y solo lo necesita si va a compilar una aplicación cliente de .NET. En este caso, lo usamos en un cliente web, así que, en lugar de eso, se necesitaría un paquete NPM de SignalR. Lo describiré con más detalle más adelante en el artículo. Tenga en cuenta que usar SignalR en el contexto de una aplicación web a partir del modelo de aplicación de MVC no es un requisito. Puede usar los servicios de la biblioteca de SignalR directamente desde la aplicación de consola de ASP.NET Core y también puede hospedar la parte de servidor de SignalR en una aplicación de consola.

No es de extrañar que la clase inicial de la aplicación deba contener cierto código específico. En concreto, agregará el servicio de SignalR a la colección de servicios del sistema y lo configurará para un uso real. En la Figura 1 se presenta el estado típico de una clase inicial que usa SignalR.

Figura 1 Clase inicial de una aplicación ASP.NET Core SignalR

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddMvc();
    services.AddSignalR();
  }
  public void Configure(IApplicationBuilder app)
  {
    app.UseStaticFiles();
    app.UseDeveloperExceptionPage();
    // SignalR
    app.UseSignalR(routes =>
    {
      routes.MapHub<UpdaterHub>("/updaterDemo");
      routes.MapHub<ProgressHub>("/progressDemo");
    });
    app.UseMvcWithDefaultRoute();
  }
}

La configuración del servicio SignalR consiste en la definición de una o más rutas de servidor que enlazan con uno o más puntos de conexión dentro del entorno del lado servidor. El método MapHub<T> vincula los nombres especificados, que formarán parte de una dirección URL de solicitud, a una instancia de una clase de centro. La clase de centro es el corazón de la implementación del protocolo SignalR y es donde se gestionan las llamadas del cliente. Puede crear un centro para cada grupo de llamadas relacionadas lógicamente que el lado del servidor intente aceptar y controlar. Una conversación de SignalR está formada por mensajes que se intercambian entre las dos partes y cada parte puede llamar a métodos de la otra parte y no recibir respuesta, recibir una o varias respuestas o recibir solo una notificación de error. La implementación del servidor de SignalR de ASP.NET Core expondrá uno o más centros. En la Figura 1, tiene dos clases de centro: UpdaterHub y ProgressHub. Están enlazadas con cadena únicas que se usarán internamente para formar el destino de URL de las llamadas reales.

Clase de centro

La clase de centro de una aplicación de SignalR es una clase sencilla que hereda de la clase de centro base. La clase base tiene la única finalidad de ahorrar a los desarrolladores el trabajo de escribir una y otra vez el mismo código reutilizable. La clase base solo proporciona cierta infraestructura, pero ningún comportamiento predefinido. En particular, define a los miembros de la Figura 2.

Figura 2 Miembros de la clase base de centro

Miembro Descripción
Clientes Propiedad que expone la lista actual de clientes que administra el centro.
Contexto Propiedad que expone el contexto del autor de la llamada actual, con información sobre el id. de la conexión y las notificaciones del usuario, si están disponibles.
Grupos Propiedad que expone varios subconjuntos de clientes que se pueden haber definido mediante programación como grupos dentro de la lista completa de clientes. Normalmente, un grupo se crea como una forma de difundir mensajes específicos a un público seleccionado.
OnConnectedAsync Método virtual que se invoca cuando un cliente nuevo se conecta al centro.
OnDisconnectedAsync Método virtual que se invoca cuando un cliente nuevo se desconecta del centro.

La clase de centro más sencilla es tan mínima como se muestra a continuación:

public class ProgressHub : Hub
{
}

Resulta interesante que es solo la forma del centro que se usa desde un método de controlador en el contexto de una aplicación MVC de ASP.NET Core. Casi todos los ejemplos que pueda encontrar sobre ASP.NET Core SignalR (incluido el ejemplo del chat) tienden a mostrar un enlace directo y bidireccional entre el cliente y el centro sin ningún tipo de intermediación por parte del controlador. En este caso, el centro tendrá una forma un poco más definida:

public class SampleChat : Hub
{     
  // Invoked from outside the hub
  public void Say(string message)
  {
    // Invoke method on listening client(s)
    return Clients.All.InvokeAsync("Said", message);
  }
}

Al contrario de lo que sucede con el ejemplo del chat de SignalR canónico recombinado en docenas de entradas de blog, el ejemplo que presento aquí no configura ninguna conversación bidireccional entre el cliente y el servidor. La conexión se establece desde el cliente, pero, después de eso, el cliente no enviará más solicitudes. En su lugar, el servidor supervisará el progreso de una tarea y enviará los cambios al cliente cuando corresponda. En otras palabras, la clase de centro debe tener métodos públicos como el código anterior solo si el caso práctico requiere que el cliente los llame directamente. Si no queda claro, el ejemplo siguiente arroja luz sobre el tema.

Supervisar una tarea remota

Este es el escenario: Una aplicación de ASP.NET Core presenta al usuario una interfaz HTML para que el usuario desencadene una tarea remota (por ejemplo, la creación de un informe) que puede tardar en completarse. Debido a esto, como desarrollador, es recomendable mostrar una barra de progreso para proporcionar comentarios continuos (consulte la Figura 3).

Uso de SignalR para supervisar el progreso de una tarea remota
Figura 3 Uso de SignalR para supervisar el progreso de una tarea remota

Como habrá podido adivinar, en este ejemplo, tanto el cliente como el servidor están configurando una conversación de SignalR en directo en el contexto del mismo proyecto ASP.NET Core. En esta fase del desarrollo, tiene un proyecto MVC totalmente funcional recién ampliado con el código inicial de la Figura 1. Configuremos el marco del cliente. Debe hacer este trabajo de configuración en todas las vistas de Razor (o HTML estándar) que interactúen con un punto de conexión SignalR.

Para comunicarse con un punto de conexión SignalR desde un explorador web, lo primero que tiene que hacer es agregar una referencia a la biblioteca de cliente JavaScript de SignalR:

<script src="~/scripts/signalr.min.js">
</script>

Puede obtener este archivo JavaScript de varias maneras. La más recomendada es a través de la herramienta Node.js Package Manager (NPM) disponible en prácticamente cualquier máquina de desarrollo, especialmente después de Visual Studio 2017. En NPM, busque el cliente de ASP.NET Core SignalR denominado @aspnet/signalr e instálelo. Esto copia un grupo de archivos JavaScript en el disco. En la mayoría de escenarios, solo se necesita uno de ellos. En cualquier caso, es cuestión de vincular un archivo JavaScript y podrá obtenerlo de muchas otras formas; por ejemplo, copiándolo de un proyecto anterior de ASP.NET Core SignalR. Sin embargo, NPM es la única forma compatible que ofrece el equipo para obtener el script. Tenga en cuenta, además, que SignalR de ASP.NET Core ya no depende de jQuery.

En la aplicación cliente, también necesita otro segmento más específico de código JavaScript. En concreto, necesita código como el que se muestra a continuación:

var progressConnection =
  new signalR.HubConnection("/progressDemo");
progressConnection.start();

Crea una conexión con el centro de SignalR que coincide con la ruta de acceso especificada. En concreto, el nombre que pasa como argumento a HubConnection debe ser uno de los nombres asignados a una ruta en la clase inicial. Internamente, el objeto HubConnection prepara una cadena URL que se obtiene de la concatenación de la URL de servidor actual y el nombre asignado. Esta dirección URL solo se procesa si coincide con una de las rutas configuradas. Tenga en cuenta también que, si el cliente y el servidor no son la misma aplicación web, se debe pasar a HubConnection la dirección URL completa de la aplicación ASP.NET Core que hospeda el centro de SignalR, además del nombre del centro.

A continuación, el objeto de conexión del centro de JavaScript se debe abrir a través del método inicial. Puede usar promesas de JavaScript (concretamente, el método then) o async/await en TypeScript para realizar acciones subsiguientes como inicializar la interfaz del usuario. La conexión SignalR se identifica de forma única mediante un identificador de cadena.

Es importante tener en cuenta que ASP.NET Core SignalR ya no admite la reconexión automática en caso de error de conexión de transporte o de servidor. En versiones anteriores, en caso de error del servidor, el cliente intenta volver a establecer una conexión de acuerdo con un algoritmo de programación y, si es correcta, reabre una conexión con el mismo identificador. En SignalR Core, si se pierde la conexión, el cliente solo puede volverla a iniciar a través del inicio del método, lo que provoca una instancia de conexión distinta con un identificador de conexión distinto.

API de devolución de llamada de cliente

Otro segmento fundamental del código JavaScript que necesita es el JavaScript al que devolverá la llamada el centro para actualizar la interfaz y reflejar en el cliente que se están haciendo progresos en el servidor. La forma de escribir el código es distinta en ASP.NET Core SignalR respecto a versiones anteriores, pero el propósito es exactamente el mismo. En nuestro ejemplo, tenemos tres métodos a los que se podría devolver la llamada desde el servidor: initProgressBar, updateProgressBar y clearProgressBar. Por supuesto, los nombres y las firmas son arbitrarios. Vea una implementación de muestra:

progressConnection.on("initProgressBar", () => {
  setProgress(0);
  $("#notification").show();
});
progressConnection.on("updateProgressBar", (perc) => {
  setProgress(perc);
});
progressConnection.on("clearProgressBar", () => {
  setProgress(100);
  $("#notification").hide();
});

Por ejemplo, cuando se devuelve la llamada al método initProgressBar desde el servidor, la función auxiliar setProgress de JavaScript configura la barra de progreso (la demostración usa un componente de barra de progreso de arranque) y la muestra. Tenga en cuenta que la biblioteca jQuery se usa en el código, pero solo para actualizar la UI. Como ya se ha mencionado, la biblioteca SignalR Core cliente ya no es un complemento de jQuery. En otras palabras; si la UI se basa, por ejemplo, en Angular, puede que no necesite usar jQuery para nada.

Eventos del lado servidor

La pieza que falta es la parte de la solución que decide cuándo es hora de invocar una función cliente. Existen dos escenarios principales. En uno, el cliente invoca una acción de servidor a través de una API web o un punto de conexión de controlador. En el otro, el cliente invoca el centro directamente. Al final, se trata de dónde programar la tarea que devuelve la llamada al cliente.

En el ejemplo de chat canónico, todo sucede dentro del centro: aplicación de cualquier lógica requerida y distribución de mensajes a la conexión correspondiente. Supervisar una tarea remota es otra cosa. Se da por sentado que existe algún proceso empresarial en segundo plano que notifica su progreso de alguna forma. Técnicamente, puede tener este proceso programado en el centro y configurar desde allí una conversación con la UI de cliente. También puede hacer que desencadene el proceso un controlador (API) y usar el centro solo como una forma de pasar eventos al cliente. De un modo más realista del que se muestra en el ejemplo, tiene el proceso programado en un nivel inferior al nivel de los controladores.

En resumen, define la clase de centro y la pone a disposición donde puede decidir cuándo y si se invocan funciones cliente. Lo más interesante es lo que hace falta para insertar la instancia del centro en un controlador u otra clase empresarial. La demostración inyecta el centro en un controlador, pero haría exactamente lo mismo para otra clase de nivel más profundo. El elemento TaskController de ejemplo se invoca mediante JavaScript directamente desde el cliente para desencadenar la tarea remota cuyos progresos moverán la barra de progreso:

public class TaskController : Controller
{
  private readonly IHubContext<ProgressHub> _progressHubContext;
  public TaskController(IHubContext<ProgressHub> progressHubContext)
  {
    _progressHubContext = progressHubContext;
  }
  public IActionResult Lengthy()
  {
    // Perform the task and call back
  }
}

Inserta un centro en un controlador o en cualquier otra clase a través de la interfaz IHubContext<THub>. La interfaz IHubContext resume la instancia del centro, pero no le otorga acceso directo a esta. Desde aquí, puede volver a enviar mensajes a la UI, pero no puede acceder, por ejemplo, al identificador de conexión. Digamos que la tarea remota se realiza con el método más largo y desde allí quiere actualizar la barra de progreso del cliente:

progressHubContext
  .Clients
  .Client(connId)
  .InvokeAsync("updateProgressBar", 45);

El identificador de conexión se puede recuperar desde dentro de la clase del centro, pero no desde dentro de un contexto de centro genérico, como en este ejemplo. Así pues, la forma más sencilla es que el método cliente pase la cadena de conexión cuando inicie la tarea remota:

public void Lengthy([Bind(Prefix="id")] string connId) { … }

Al final, la clase de controlador recibe el id. de conexión de SignalR, se le inserta el contexto del centro y actúa a través del contexto llamando a métodos a través de una API genérica sin tipos: el método InvokeAsync. Usado de este modo, no es necesario que haya métodos en la clase del centro. Si le parece sorprendente, eche un vistazo al código de bit.ly/2DWd8SV.

Resumen

En este artículo se explica el uso de ASP.NET Core SignalR en el contexto de una aplicación web para supervisar una tarea remota. El centro está prácticamente vacío, ya que la lógica de notificación se integra en el controlador y se orquesta a través del contexto de centro que se inserta mediante DI. Este es solo el punto de partida de un recorrido más largo con ASP.NET Core SignalR. Próximamente, indagaré más en la infraestructura y exploraré los centros con tipos.


Dino Esposito ha escrito más de 20 libros y 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 de Microsoft por su ayuda en la revisión de este artículo: Andrew Stanton-Nurse


Discuta sobre este artículo en el foro de MSDN Magazine