Inyección de dependencias en Hubs de SignalR

Cuando estamos implementando Hubs de SignalR, podemos encontrarnos fácilmente con que éstos necesitan utilizar componentes externos para llevar a cabo su tarea. Por ejemplo, es bastante probable que un servicio en tiempo real proporcionado por un Hub tenga que utilizar una clase de servicios o cualquier otro componente externo de una aplicación, como en el siguiente código:

1
2
3
4
5
6
7
8
9
10
11
public class MyHub: Hub
{
    public Task sendMessage(string text)
    {
        using (var services = new LogServices())
        {
            services.Log(Context.ConnectionId, text);
            return Clients.All.receiveMessage(text);
        }
    }
}

Como puede intuirse, en el interior del método sendMessage() se utiliza un componente externo, implementado en la clase LogService, para guardar una traza con los mensajes enviados a través de nuestro sistema.

Aunque funcionaría bien, el código anterior introduciría un acoplamiento demasiado fuerte entre el Hub y la clase LogService, y está claro que eso no puede ser bueno. Estaríamos haciendo totalmente dependiente la primera clase de la segunda y esto podría afectarnos en el futuro vistas al mantenimiento y evolución del software, o imposibilitar la realización de pruebas unitarias de forma correcta.

1. Desacoplando componentes

Como buenos seguidores que somos de los principios SOLID, seguro que ya sabemos que la solución pasa por aplicar Inyección de Dependencias (la “D” de SOLID) y la abstracción de la implementación concreta mediante interfaces. Lo que estaríamos buscando es una codificación funcionalmente equivalente a la anterior pero con un mínimo nivel de acoplamiento, como la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyHub: Hub
{
    private readonly ILogServices _logServices;
 
    public MyHub(ILogServices logServices)
    {
        _logServices = logServices;
    }
 
    public Task sendMessage(string text)
    {
        _logServices.Log(Context.ConnectionId, text);
        return Clients.All.receiveMessage(text);
    }
}

Observad las dos diferencias principales respecto al código que vimos al principio del post:

  • MyHub ha dejado de depender de la clase LogService. Cualquier referencia a las operaciones que necesitamos realizar se hacen a través del interfaz ILogService, que será el encargado de abstraernos de la implementación concreta a utilizar.  
  • No instanciamos ningún objeto. El componente externo que proporciona las operaciones requeridas por el interfaz nos llega en el constructor del Hub y lo guardamos en un miembro privado de la clase para usarlo posteriormente.

Con esta implementación hemos conseguido un método sendMessage() mucho más legible y hemos roto la fuerte dependencia entre éste y la clase que usaremos para guardar el mensaje en el log, lo cual podría permitirnos en el futuro sustituir su implementación de forma transparente siempre que se siga cumpliendo el contrato establecido por el interfaz. Y, por supuesto, podríamos hacer pruebas unitarias sobre la clase muy fácilmente.

Sin embargo, el Hub anterior no funcionaría de forma directa puesto que SignalR no sería capaz de crear una instancia de MyHub ante la llegada de un mensaje desde el lado cliente al no existir un constructor sin parámetros. Es un fenómeno similar al que encontramos en sistemas ASP.NET MVC o WebAPI cuando nuestros controladores no tienen constructores públicos sin parámetros.

¿Y cómo solucionamos esto?

2. Registrando el Hub en el Dependency Resolver

SignalR, como otras tecnologías pertenecientes al stack ASP.NET como MVC o WebAPI, utiliza en una gran cantidad de puntos un mecanismo llamado Dependency Resolver para crear instancias de clases que necesita para trabajar. El Dependency Resolver utilizado por SignalR es un objeto de tipo IDependencyResolver  que se encuentra almacenado en la propiedad GlobalHost.DependencyResolver.

Su funcionamiento, a grandes rasgos, es el siguiente: cuando SignalR necesita instanciar un objeto de cualquier tipo, lo primero que hace es ponerse en contacto con el Dependency Resolver para ver si éste puede ofrecérsela (algo así como “oye, ¿me puedes facilitar una instancia del tipo XYZ?”).

Si el Dependency Resolver puede proporcionar una instancia del tipo que se le está solicitando, la retornaría directamente (“pues sí, ahí la llevas”). En caso contrario retornaría un valor nulo, con lo cual SignalR sería el encargado de crear la instancia.

Y los Hub no son una excepción. Si un cliente se conecta, desconecta, o realiza una llamada a un método de MyHub, SignalR llamará al mecanismo de resolución de dependencias para ver si puede proporcionarle una instancia de dicho tipo, que es la que procesará el mensaje. Lo único que tenemos que hacer para que todo funcione es instruir adecuadamente al Dependency Resolver, es decir, decirle cómo debe instanciarla cuando esta solicitud se produzca.

Esto podríamos conseguirlo de la siguiente forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Global : System.Web.HttpApplication
{
 
    protected void Application_Start(object sender, EventArgs e)
    {
        GlobalHost.DependencyResolver.Register(
            typeof (MyHub),
            () => new MyHub(new LogServices())
        );
        RouteTable.Routes.MapHubs();
    }
 
}

Observad que el método Register() permite asociar un tipo de datos (MyHub) a un delegado, en este caso implementado mediante una función lambda que retorna el objeto instanciado, y donde ya estamos pasando a su constructor las dependencias que requiere. Cuando alguien solicite al Dependency Resolver una instancia de MyHub, se le retornará el resultado de ejecutar la función especificada.

Por supuesto sería totalmente posible utilizar un contenedor de inversión de control que se encargara de parte del trabajo sucio asociado a la instanciación de objetos y, sobre todo, si se trata de grafos complejos. El siguiente ejemplo muestra cómo podríamos integrar en el código anterior un kernel de Ninject, aunque sería bastante parecido si usamos cualquier otro:

1
2
3
4
5
6
7
8
9
10
11
12
protected void Application_Start(object sender, EventArgs e)
{
    var kernel = new StandardKernel();
    kernel.Bind<ILogServices>().To<LogServices>();
    // ... register other dependencies
 
    GlobalHost.DependencyResolver.Register(
        typeof(MyHub),
        () => kernel.Get<MyHub>()
    );
    RouteTable.Routes.MapHubs();
}

Aunque la verdad es que si vamos a utilizar un contenedor de IoC, la opción más razonable sería crear un Dependency Resolver personalizado y decirle a SignalR que lo utilice…

3. Creando un Dependency Resolver personalizado

La propiedad GlobalHost.DependencyResolver que hemos usado anteriormente para acceder al componente encargado de resolver las dependencias es de lectura/escritura, lo que sugiere la posibilidad de sustituirlo por otro más a nuestra medida.

En este caso, el componente utilizado debe implementar el interfaz IDependencyResolver presente en el espacio de nombres Microsoft.AspNet.SignalR, aunque nos costará menos trabajo crear una clase que herede de DefaultDependencyResolver, la implementación por defecto que viene de serie con SignalR, en la que sobrescribiremos los métodos cuyo comportamiento nos interese modificar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyDependencyResolver : DefaultDependencyResolver
{
    private IKernel _kernel;
 
    public MyDependencyResolver(IKernel kernel)
    {
        _kernel = kernel;
    }
 
    public override object GetService(Type serviceType)
    {
        var instance = _kernel.TryGet(serviceType);
        return instance ?? base.GetService(serviceType);
    }
 
    public  override IEnumerable<object> GetServices(Type serviceType)
    {
        var instances = base.GetServices(serviceType);
        return _kernel.GetAll(serviceType).Concat(instances);
    }
}

Una vez creado nuestro componente de resolución de dependencias, podemos crear una instancia y establecerla en GlobalHost.DependencyResolver o bien pasarla como parámetro en el momento de registrar las rutas de los Hubs, quedando así un código de inicialización más simple:

1
2
3
4
5
6
7
8
protected void Application_Start(object sender, EventArgs e)
{
    var kernel = new StandardKernel();
    kernel.Bind<ILogServices>().To<LogServices>();
    // ... register other dependencies
 
    RouteTable.Routes.MapHubs(new MyDependencyResolver(kernel));
}

De nuevo, estos ejemplos están implementados usando Ninject, pero realmente costaría bastante poco trabajo adaptarlos a cualquier otro contenedor IoC.

Publicado en Variable not found.

Simular una conexión lenta con Fiddler

Me abuuuuurroNormalmente hacemos pruebas de nuestros desarrollos web sobre nuestro propio equipo, donde la transferencia de datos es prácticamente inmediata, en servidores de prueba ubicados en una red de área local, o incluso sobre un servidor de producción al que accedemos mediante una conexión a Internet de gran capacidad.

Sin embargo, nuestras aplicaciones web son muy diferentes cuando el cliente no dispone de una conexión de alta velocidad. Lo que nosotros percibimos en tiempo de desarrollo como una maravilla de agilidad, espectacularidad y facilidad de uso, puede ser un auténtico desastre para el cliente si no hemos tenido en cuenta que no todo el mundo puede disfrutar de conexiones de alta calidad.

Por esta razón, es interesante realizar de vez en cuando pruebas de nuestros sistemas reduciendo de forma artificial el ancho de banda disponible, de forma que podamos detectar en qué puntos podemos mejorar la experiencia del usuario en estos escenarios.

FiddlerUna forma muy sencilla para hacerlo es utilizando Fiddler. Probablemente todos lo conoceréis, pues es una de esas herramientas gratuitas que con el tiempo se han hecho indispensables para los desarrolladores, ayudándonos a ver qué es lo que pasa por debajo de aplicaciones que utilizan HTTP para enviar o recibir datos desde el exterior.

Básicamente se trata de un proxy que intercepta todas las conexiones HTTP/HTTPS originadas en nuestro ordenador permitiéndonos guardaras en un registro, inspeccionarlas en detalle, repetirlas, e incluso modificar su contenido sin demasiado esfuerzo. Además, incluye un potente mecanismo que permite extenderlo mediante reglas programadas en lenguaje JScript.NET o plugins en forma de ensamblados .NET.

Rules > Performance > Simulate modem speeds

Fiddler trae de fábrica un conjunto de reglas que permiten personalizar el tratamiento de las peticiones HTTP que entran y salen de nuestra máquina, y una de ellas tiene el expresivo nombre “Simulate modem speeds”. Seguro que podréis intuir su utilidad 😉

Así, basta con acudir al menú “Rules” de Fiddler, abrir el submenú “Performance” y seleccionar la opción oportuna:

Rules > Performance > Simulate modem speeds

La selección de esta opción hará que la velocidad de transmisión y recepción se reduzca a la habitual en un módem de 56Kbps. En concreto:

  • Se introduce en la petición un retardo de 300ms por cada Kbyte enviado desde el navegador u aplicación cliente.
  • Por cada Kbyte recibido en la respuesta de la petición, se introduce un retardo de 150ms.

De esta forma ya podemos tener una primera solución para poder probar nuestra web a una velocidad muy inferior a lo habitual, y comenzar a ver qué puntos podríamos mejorar.

Modificar los ajustes

La lógica de cada una de esas opciones que podéis activar o desactivar en el menú “Rules” está implementada mediante scripting, lo que deja abierta la posibilidad de alterarlas para adaptarlas a nuestras necesidades.

Por ejemplo, podríamos definir tiempos de retardo a nuestro antojo, y así probar cómo funcionaría nuestro sistema con distintas configuraciones de red para determinar cuál sería el mínimo razonable para utilizar un servicio.

Customizing rules in FiddlerAccediendo al menú “Rules > Customize Rules” tendremos acceso al código fuente del script de reglas usado por Fiddler, que se abrirá automáticamente en el bloc de notas de Windows para que podamos modificarlo a nuestro antojo. Eso sí, siempre con precaución, aunque si metemos la pata podemos seguir estas instrucciones para dejarlo todo como estaba.

El código que introduce los retardos podemos localizarlo fácilmente buscando el texto “modem” en el script, y es como sigue:

1
2
3
4
5
6
if (m_SimulateModem) {
   // Delay sends by 300ms per KB uploaded.
   oSession["request-trickle-delay"] = "300";
   // Delay receives by 150ms per KB downloaded.
   oSession["response-trickle-delay"] = "150";
}

La variable m_SimulateModem contiene un valor booleano que indica si la opción del menú está marcada o no, y, en caso afirmativo, los valores de los retardos se introducen en los parámetros request-trickle-delay y response-trickle-delay del objeto oSession, que contiene distintos settings que permiten modificar  el comportamiento de Fiddler. En concreto, estos dos parámetros indican los milisegundos a esperar antes de cada kbyte enviado, y antes de cada kbyte recibido, respectivamente.

Por tanto, si queremos limitar la velocidad de descarga aproximadamente a 100KBytes/s en un entorno local, bastaría con introducir en response-trickle-delay un retardo de 10ms:

1
2
// Delay receives by 10ms per KB downloaded.
oSession["response-trickle-delay"] = "10";

Al salvar el archivo se aplicarán los cambios en la configuración.

Añadir nuevas configuraciones

Vale, con lo visto anteriormente ya podemos modificar la velocidad de descarga, pero siempre machacando los valores utilizados por la opción del menú “Simulate Modem Speeds”. Sin embargo, podemos mejorar esto bastante creando nuestras propias opciones en el menú que nos permitan activar y desactivar de forma sencilla otras configuraciones, por ejemplo para seleccionar distintas velocidades a simular.

Para ello, primero localizamos el lugar del script donde se añade la opción “Simulate Modem Speeds”, y añadimos una opción similar para nuestro objetivo:

1
2
3
4
5
6
7
// Cause Fiddler to delay HTTP traffic to simulate typical 56k modem conditions
public static RulesOption("Simulate &Modem Speeds", "Per&formance")
var m_SimulateModem: boolean = false;
 
// Cause Fiddler to delay HTTP traffic to 100Kbytes/sec
public static RulesOption("Simulate 100Kbytes/sec", "Per&formance")
var m_Simulate100K: boolean = false;

A continuación, introducimos la condición para activar los límites cuando la variable booleana que hemos definido sea true. Para no complicarnos mucho, lo hacemos justo debajo del lugar donde ya hemos visto anteriormente que se establecen estos valores dependiendo de m_SimulateModem:

1
2
3
4
5
6
if (m_Simulate100K) {
   // For example, delay sends by 40ms per KB uploaded
   oSession["request-trickle-delay"] = "40";
   // Delay receives by 10ms per KB downloaded.
   oSession["response-trickle-delay"] = "10";
}

Salvando el archivo, ya tenemos una nueva opción en el menú para activar nuestro tope de velocidad:

Custom rule

¡Y esto es todo! Así de fácil es usar Fiddler para simular lentitud en las comunicaciones y poder comprobar la usabilidad en ese tipo de escenarios.

Pero antes de que se me olvide, un último comentario importante: todo lo dicho no es válido sólo en aplicaciones web. Estas comprobaciones pueden resultar también bastante clarificadoras en otro tipo de sistemas, como pueden ser aplicaciones Windows Store o de otro tipo que accedan a recursos externos mediante HTTP. Fiddler funcionará con la mayoría de ellas sin problema.

Publicado en Variable not found.