Compartir a través de


Inserción de dependencias en SignalR 1.x

por Patrick Fletcher

Advertencia

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

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 simulados) o para cambiar el comportamiento en tiempo de ejecución. Este tutorial muestra cómo realizar la inserción de dependencias en los hubs de SignalR. También muestra cómo usar contenedores 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 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 constructor. 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 simple en SignalR

Considere la aplicación Chat del tutorial Introducción a SignalR. Aquí tiene la clase de hub 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 DI 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 SignalR no crea directamente hubs; SignalR los crea automáticamente. De manera predeterminada, SignalR espera que una clase de hub tenga un constructor sin parámetros. Sin embargo, puede registrar fácilmente una función para crear instancias de hub, y usar esta función para realizar la inserción de dependencias. Registre la función llamando a GlobalHost.DependencyResolver.Register.

protected void Application_Start()
{
    GlobalHost.DependencyResolver.Register(
        typeof(ChatHub), 
        () => new ChatHub(new ChatMessageRepository()));

    RouteTable.Routes.MapHubs();

    // ...
}

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

Contenedores 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 IoC. Un contenedor 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 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 IoC construye sus objetos por usted, lo que "invierte" el flujo de control habitual.

Uso de contenedores IoC en SignalR

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

El ejemplo StockTicker define dos clases principales:

  • StockTickerHub: la clase de hub, 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 el StockTickerHub. Usa esta interfaz para comunicarse con instancias de StockTickerHub. (Para más información, consulte Difusión del servidor con ASP.NET SignalR.)

Podemos usar un contenedor 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 StockTicker. En su lugar, usaremos siempre DI para crear el hub.

[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;
    }

    // ...

Para StockTicker, elimine la instancia singleton. Más adelante, usaremos el contenedor 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 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 el 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 displaying over Visual Studio Code, with the Refactor 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 and O K option being displayed.

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 DI puede ayudar a reducir el acoplamiento entre los componentes de su aplicación.

Agregar la biblioteca Ninject

Existen muchos contenedores 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 runtime, incluyendo instancias de hub, 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 el archivo RegisterHubs.cs. En el método RegisterHubs.Start, cree el contenedor de Ninject, que Ninject llama el 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<IHubConnectionContext>().ToMethod(context =>
    resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
).WhenInjectedInto<IStockTicker>();

Este código crea una función anónima que devuelve una 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 MapHubs:

RouteTable.Routes.MapHubs(config);

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

Aquí tiene la descripción completa del código de RegisterHubs.Start.

public static class RegisterHubs
{
    public static void Start()
    {
        var kernel = new StandardKernel();
        var resolver = new NinjectSignalRDependencyResolver(kernel);

        kernel.Bind<IStockTicker>()
            .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()
            .InSingletonScope();

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

        var config = new HubConfiguration()
        {
            Resolver = resolver
        };

        // Register the default hubs route: ~/signalr/hubs
        RouteTable.Routes.MapHubs(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 the A S P dot NET Signal R Stock Ticker Sample screen displaying in an Internet Explorer browser window.

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