Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
miércoles, 16 de enero de 2013
imageCuando 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:
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:
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:
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:
    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:
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:
    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.

4 Comentarios:

Maxxx dijo...

Hola, jose. Gran artículo, como de costumbre :)

Si me permites me gustaría hacerte una sugerencia, ¿por qué no escribes algo sobre desacoplamiento en mvc? Seguro que muchos lo agradeceríamos ;)

josé M. Aguilar dijo...

Hola, Max!

Ante todo, gracias por comentar, y por dar ideas de nuevos posts ;-)

El tema que propones es interesante. Escribiré algo al respecto en breve. :-)

Un saludo!

jmunin dijo...

Hola,

Interesante artículo, como todos los de la serie sobre SignalR. Pero tengo una duda:

Cómo haríamos la injección de dependencias si estamos usando signalr dentro de un servidor con host self, como una aplicación o un servicio windows?

Un saludo

josé M. Aguilar dijo...

Hola, jmunin!

Ante todo, gracias por tu comentario.

Con self hosting sería exactamente igual, todo lo que hemos usado es estándar de SignalR y no está acoplado a ASP.NET.

Lo único que cambia, obviamente, es el lugar de introducción del código de inicialización, que dejaría de ser el Application_Start() de ASP.NET; podrías ponerlo en cualquier de tu aplicación que se ejecute al arrancar.

Un saludo.