Inserción de dependencias en SignalR

por Patrick Fletcher

Advertencia

Esta documentación no es para la última versión de SignalR. Eche un vistazo a SignalR de ASP.NET Core.

Versiones de software usadas en este tema

Versiones anteriores de este tema

Para obtener información sobre versiones anteriores de SignalR, consulte Versiones anteriores de SignalR.

Preguntas y comentarios

Deje sus comentarios sobre este tutorial y sobre lo que podríamos mejorar en los comentarios en la parte inferior de la página. Si tiene preguntas que no están directamente relacionadas con el tutorial, puede publicarlas en el foro de SignalR de ASP.NET o en StackOverflow.com.

La inserción de dependencias es una forma de eliminar las dependencias codificadas entre objetos, facilitando el reemplazo de las dependencias de un objeto, ya sea para realizar pruebas (usando objetos ficticios) o para cambiar el comportamiento en tiempo de ejecución. Este tutorial muestra cómo realizar la inserción de dependencias en los centros de conectividad de SignalR. También muestra cómo usar contenedores de IoC con SignalR. Un contenedor IoC es un marco general para la inserción de dependencias.

¿Qué es la inserción de dependencias?

Omita esta sección si ya está familiarizado con la inserción de dependencias.

La inserción de dependencias (DI) es un patrón en el que los objetos no son responsables de crear sus propias dependencias. Este es un ejemplo sencillo para motivar la inserción de dependencias. Suponga que tiene un objeto que necesita registrar mensajes. Puede definir una interfaz de registro:

interface ILogger 
{
    void LogMessage(string message);
}

En su objeto, puede crear un ILogger para registrar mensajes:

// Without dependency injection.
class SomeComponent
{
    ILogger _logger = new FileLogger(@"C:\logs\log.txt");

    public void DoSomething()
    {
        _logger.LogMessage("DoSomething");
    }
}

Esto funciona, pero no es el mejor diseño. Si quiere reemplazar FileLogger por otra implementación de ILogger, tendrá que modificar SomeComponent. Suponiendo que muchos otros objetos usen FileLogger, tendrá que cambiarlos todos. O bien, si decide hacer de FileLogger un singleton, también tendrá que realizar cambios en toda la aplicación.

Un enfoque mejor es "insertar" un ILogger en el objeto (por ejemplo, usando un argumento de constructor):

// With dependency injection.
class SomeComponent
{
    ILogger _logger;

    // Inject ILogger into the object.
    public SomeComponent(ILogger logger)
    {
        if (logger == null)
        {
            throw new NullReferenceException("logger");
        }
        _logger = logger;
    }

    public void DoSomething()
    {
        _logger.LogMessage("DoSomething");
    }
}

Ahora el objeto no es responsable de seleccionar qué ILogger usar. Puede cambiar las implementaciones de ILogger sin cambiar los objetos que dependen de él.

var logger = new TraceLogger(@"C:\logs\log.etl");
var someComponent = new SomeComponent(logger);

Este patrón se denomina inserción de constructores. Otro patrón es la inserción de establecedores, donde se establece la dependencia a través de un método de establecedor o una propiedad.

Inserción de dependencias simples en SignalR

Considere la aplicación Chat del tutorial Introducción a SignalR. Aquí tiene la clase del centro de conectividad de esa aplicación:

public class ChatHub : Hub
{
    public void Send(string name, string message)
    {
        Clients.All.addMessage(name, message);
    }
}

Supongamos que quiere almacenar los mensajes de chat en el servidor antes de enviarlos. Puede definir una interfaz que abstraiga esta funcionalidad y usar la inserción de dependencias para insertar la interfaz en la clase ChatHub.

public interface IChatRepository
{
    void Add(string name, string message);
    // Other methods not shown.
}

public class ChatHub : Hub
{
    private IChatRepository _repository;

    public ChatHub(IChatRepository repository)
    {
        _repository = repository;
    }

    public void Send(string name, string message)
    {
        _repository.Add(name, message);
        Clients.All.addMessage(name, message);
    }

El único problema es que una aplicación de SignalR no crea directamente centros; SignalR los crea automáticamente. De manera predeterminada, SignalR espera que una clase de centro de conectividad tenga un constructor sin parámetros. Sin embargo, puede registrar fácilmente una función para crear instancias de centro de conectividad, y usar esta función para realizar la inserción de dependencias. Registre la función llamando a GlobalHost.DependencyResolver.Register.

public void Configuration(IAppBuilder app)
{
    GlobalHost.DependencyResolver.Register(
        typeof(ChatHub), 
        () => new ChatHub(new ChatMessageRepository()));

    App.MapSignalR();

    // ...
}

Ahora SignalR invocará esta función anónima siempre que necesite crear una instancia de ChatHub.

Contenedores de IoC

El código anterior está bien para casos sencillos. Pero aún así tenía que escribir esto:

... new ChatHub(new ChatMessageRepository()) ...

En una aplicación compleja con muchas dependencias, puede que necesite escribir mucho de este código de "cableado". Este código puede ser difícil de mantener, especialmente si las dependencias están anidadas. También es difícil realizar pruebas unitarias.

Una solución es usar un contenedor de IoC. Un contenedor de IoC es un componente de software que se encarga de administrar las dependencias. Usted registra tipos en el contenedor y después lo usa para crear objetos. El contenedor calcula automáticamente las relaciones de dependencia. Muchos contenedores de IoC también le permiten controlar cosas como la duración y el ámbito de los objetos.

Nota:

"IoC" significa "inversión de control", que es un patrón general en el que un marco llama al código de la aplicación. Un contenedor de IoC construye los objetos automáticamente, lo que "invierte" el flujo de control habitual.

Uso de contenedores de IoC en SignalR

La aplicación Chat es probablemente demasiado simple para beneficiarse de un contenedor de IoC. En su lugar, veamos el ejemplo de StockTicker.

El ejemplo de StockTicker define dos clases principales:

  • StockTickerHub: la clase de centro de conectividad, que administra las conexiones de los clientes.
  • StockTicker: singleton que contiene los precios de las acciones y los actualiza periódicamente.

StockTickerHub contiene una referencia al singleton StockTicker, mientras que StockTicker contiene una referencia al IHubConnectionContext para StockTickerHub. Usa esta interfaz para comunicarse con instancias de StockTickerHub. (Para más información, consulte Difusión del servidor con SignalR de ASP.NET.)

Podemos usar un contenedor de IoC para desenredar un poco estas dependencias. En primer lugar, simplifiquemos las clases StockTickerHub y StockTicker. En el siguiente código, he marcado como comentario las partes que no necesitamos.

Elimine el constructor sin parámetros de StockTickerHub. En su lugar, usaremos siempre la inserción de dependencias para crear el centro de conectividad.

[HubName("stockTicker")]
public class StockTickerHub : Hub
{
    private readonly StockTicker _stockTicker;

    //public StockTickerHub() : this(StockTicker.Instance) { }

    public StockTickerHub(StockTicker stockTicker)
    {
        if (stockTicker == null)
        {
            throw new ArgumentNullException("stockTicker");
        }
        _stockTicker = stockTicker;
    }

    // ...

En StockTicker, elimine la instancia singleton. Más adelante, usaremos el contenedor de IoC para controlar la duración de StockTicker. Además, haga que el constructor sea público.

public class StockTicker
{
    //private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(
    //    () => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

    // Important! Make this constructor public.
    public StockTicker(IHubConnectionContext<dynamic> clients)
    {
        if (clients == null)
        {
            throw new ArgumentNullException("clients");
        }

        Clients = clients;
        LoadDefaultStocks();
    }

    //public static StockTicker Instance
    //{
    //    get
    //    {
    //        return _instance.Value;
    //    }
    //}

A continuación, podemos refactorizar el código creando una interfaz para StockTicker. Usaremos esta interfaz para desacoplar StockTickerHub de la clase StockTicker.

Visual Studio facilita este tipo de refactorización. Abra el archivo StockTicker.cs, haga clic con el botón derecho en la declaración de clase de StockTicker y seleccione Refactorizar ... Extraer interfaz.

Screenshot of the right-click dropdown menu over Visual Studio code with the Refractor and Extract Interface options being highlighted.

En el cuadro de diálogo Extraer interfaz, haga clic en Seleccionar todo. Deje los demás valores predeterminados. Haga clic en OK.

Screenshot of the Extract Interface dialog with the Select All option being highlighted, with all the available options being selected.

Visual Studio crea una nueva interfaz denominada IStockTicker, y también cambia StockTicker para que derive de IStockTicker.

Abra el archivo IStockTicker.cs y cambie la interfaz a pública.

public interface IStockTicker
{
    void CloseMarket();
    IEnumerable<Stock> GetAllStocks();
    MarketState MarketState { get; }
    void OpenMarket();
    void Reset();
}

En la clase StockTickerHub, cambie las dos instancias de StockTicker por IStockTicker:

[HubName("stockTicker")]
public class StockTickerHub : Hub
{
    private readonly IStockTicker _stockTicker;

    public StockTickerHub(IStockTicker stockTicker)
    {
        if (stockTicker == null)
        {
            throw new ArgumentNullException("stockTicker");
        }
        _stockTicker = stockTicker;
    }

Crear una interfaz de IStockTicker no es estrictamente necesario, pero quería mostrar cómo la inserción de dependencias puede ayudar a reducir el acoplamiento entre los componentes de su aplicación.

Agregar la biblioteca Ninject

Existen muchos contenedores de IoC de código abierto para .NET. Para este tutorial, usaré Ninject. (Otras bibliotecas populares son Castle Windsor, Spring.Net, Autofac, Unity y StructureMap).

Use el Administrador de paquetes NuGet para instalar la biblioteca Ninject. En Visual Studio, en el menú Herramientas, seleccione Administrador de paquetes NuGet>Consola del Administrador de paquetes. En la ventana Consola del Administrador de paquetas , escriba el siguiente comando:

Install-Package Ninject -Version 3.0.1.10

Reemplazar el solucionador de dependencias de SignalR

Para usar Ninject dentro de SignalR, cree una clase que derive de DefaultDependencyResolver.

internal class NinjectSignalRDependencyResolver : DefaultDependencyResolver
{
    private readonly IKernel _kernel;
    public NinjectSignalRDependencyResolver(IKernel kernel)
    {
        _kernel = kernel;
    }

    public override object GetService(Type serviceType)
    {
        return _kernel.TryGet(serviceType) ?? base.GetService(serviceType);
    }

    public override IEnumerable<object> GetServices(Type serviceType)
    {
        return _kernel.GetAll(serviceType).Concat(base.GetServices(serviceType));
    }
}

Esta clase invalida los métodos GetService y GetServices de DefaultDependencyResolver. SignalR usa estos métodos para crear varios objetos en tiempo de ejecución, incluyendo instancias de centro de conectividad, así como varios servicios usados internamente por SignalR.

  • El método GetService crea una única instancia de un tipo. Invalide este método para llamar al método TryGet del kernel de Ninject. Si ese método devuelve NULL, vuelva al solucionador predeterminado.
  • El método GetServices crea una colección de objetos de un tipo especificado. Invalide este método para concatenar los resultados de Ninject con los resultados del solucionador predeterminado.

Configurar enlaces de Ninject

Ahora usaremos Ninject para declarar enlaces de tipos.

Abra la clase Startup.cs de la aplicación (que creó manualmente según las instrucciones del paquete de readme.txt o que se creó agregando autenticación al proyecto). En el método Startup.Configuration, cree el contenedor de Ninject, que Ninject llama kernel.

var kernel = new StandardKernel();

Cree una instancia de nuestro solucionador de dependencias personalizado:

var resolver = new NinjectSignalRDependencyResolver(kernel);

Cree un enlace para IStockTicker de la manera siguiente:

kernel.Bind<IStockTicker>()
    .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()  // Bind to StockTicker.
    .InSingletonScope();  // Make it a singleton object.

Este código está diciendo dos cosas. En primer lugar, siempre que la aplicación necesite un IStockTicker, el kernel debería crear una instancia de StockTicker. En segundo lugar, la clase StockTicker debería crearse como un objeto singleton. Ninject creará una instancia del objeto y devolverá la misma instancia para cada solicitud.

Cree un enlace para IHubConnectionContext como se indica a continuación:

kernel.Bind(typeof(IHubConnectionContext<dynamic>)).ToMethod(context =>
                    resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
                     ).WhenInjectedInto<IStockTicker>();

Este código crea una función anónima que devuelve un IHubConnection. El método WhenInjectedInto indica a Ninject que use esta función solo al crear instancias de IStockTicker. La razón es que SignalR crea instancias de IHubConnectionContext internamente, y no queremos invalidar cómo las crea SignalR. Esta funcionalidad solo se aplica a nuestra clase StockTicker.

Pase el solucionador de dependencias al método MapSignalR agregando una configuración del centro de conectividad:

var config = new HubConfiguration();
config.Resolver = resolver;
Microsoft.AspNet.SignalR.StockTicker.Startup.ConfigureSignalR(app, config);

Actualice el método Startup.ConfigureSignalR de la clase Startup del ejemplo con el nuevo parámetro:

public static void ConfigureSignalR(IAppBuilder app, HubConfiguration config)
{
    app.MapSignalR(config);
}

Ahora SignalR usará el solucionador especificado en MapSignalR en lugar del solucionador predeterminado.

Aquí tiene la descripción completa del código de Startup.Configuration.

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888

        var kernel = new StandardKernel();
        var resolver = new NinjectSignalRDependencyResolver(kernel);

        kernel.Bind<IStockTicker>()
            .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()  // Bind to StockTicker.
            .InSingletonScope();  // Make it a singleton object.

        kernel.Bind(typeof(IHubConnectionContext<dynamic>)).ToMethod(context =>
                resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
                    ).WhenInjectedInto<IStockTicker>();

        var config = new HubConfiguration();
        config.Resolver = resolver;
        Microsoft.AspNet.SignalR.StockTicker.Startup.ConfigureSignalR(app, config);
    }
}

Para ejecutar la aplicación StockTicker en Visual Studio, pulse F5. En la ventana del explorador, vaya a http://localhost:*port*/SignalR.Sample/StockTicker.html.

Screenshot of an Internet Explorer browser window, displaying the A S P dot NET Signal R Stock Ticker Sample webpage.

La aplicación tiene exactamente la misma funcionalidad que antes. (Para una descripción, consulte Transmisión del servidor con SignalR de ASP.NET). No hemos cambiado el comportamiento; solo hemos hecho que el código sea más fácil de probar, mantener y evolucionar.