Inserimento delle dipendenze in SignalR 1.x

di Patrick Fletcher

Avviso

Questa documentazione non è per la versione più recente di SignalR. Esaminare ASP.NET Core SignalR.

L'inserimento delle dipendenze è un modo per rimuovere le dipendenze hardcoded tra oggetti, semplificando la sostituzione delle dipendenze di un oggetto, per il test (usando oggetti fittizi) o per modificare il comportamento di runtime. Questa esercitazione illustra come eseguire l'inserimento delle dipendenze in hub SignalR. Mostra anche come usare i contenitori IoC con SignalR. Un contenitore IoC è un framework generale per l'inserimento delle dipendenze.

Che cos'è l'inserimento delle dipendenze?

Ignorare questa sezione se si ha già familiarità con l'inserimento delle dipendenze.

L'inserimento delle dipendenze è un modello in cui gli oggetti non sono responsabili della creazione delle proprie dipendenze. Ecco un semplice esempio per motivare DI. Si supponga di avere un oggetto che deve registrare i messaggi. È possibile definire un'interfaccia di registrazione:

interface ILogger 
{
    void LogMessage(string message);
}

Nell'oggetto è possibile creare un ILogger oggetto per registrare i messaggi:

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

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

Questo funziona, ma non è il design migliore. Se si vuole sostituire FileLogger con un'altra ILogger implementazione, sarà necessario modificare SomeComponent. Supponendo che molti altri oggetti usino FileLogger, sarà necessario modificarli tutti. In alternativa, se si decide di apportare FileLogger un singleton, è necessario apportare modifiche in tutta l'applicazione.

Un approccio migliore consiste nell'inserire un ILogger oggetto nell'oggetto, ad esempio usando un argomento del costruttore:

// 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");
    }
}

Ora l'oggetto non è responsabile della selezione ILogger da usare. È possibile cambiare ILogger implementazioni senza modificare gli oggetti che dipendono da esso.

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

Questo modello viene chiamato inserimento del costruttore. Un altro modello è setter injection, in cui si imposta la dipendenza tramite un metodo o una proprietà setter.

Inserimento di dipendenze semplice in SignalR

Prendere in considerazione l'applicazione Chat dall'esercitazione Introduzione con SignalR. Ecco la classe hub da tale applicazione:

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

Si supponga di voler archiviare i messaggi di chat nel server prima di inviarli. È possibile definire un'interfaccia che astrae questa funzionalità e usare DI per inserire l'interfaccia nella ChatHub classe.

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

L'unico problema è che un'applicazione SignalR non crea direttamente hub; SignalR li crea per te. Per impostazione predefinita, SignalR prevede che una classe hub abbia un costruttore senza parametri. Tuttavia, è possibile registrare facilmente una funzione per creare istanze dell'hub e usare questa funzione per eseguire DI. Registrare la funzione chiamando GlobalHost.DependencyResolver.Register.

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

    RouteTable.Routes.MapHubs();

    // ...
}

SignalR richiama ora questa funzione anonima ogni volta che deve creare un'istanza ChatHub .

Contenitori IoC

Il codice precedente è corretto per i casi semplici. Ma è ancora necessario scrivere questo:

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

In un'applicazione complessa con molte dipendenze, potrebbe essere necessario scrivere un sacco di codice "cablaggio". Questo codice può essere difficile da gestire, soprattutto se le dipendenze vengono annidate. È anche difficile unit test.

Una soluzione consiste nell'usare un contenitore IoC. Un contenitore IoC è un componente software responsabile della gestione delle dipendenze. È possibile registrare i tipi con il contenitore e quindi usare il contenitore per creare oggetti. Il contenitore individua automaticamente le relazioni di dipendenza. Molti contenitori IoC consentono anche di controllare elementi come la durata dell'oggetto e l'ambito.

Nota

"IoC" è "inversione del controllo", ovvero un modello generale in cui un framework chiama nel codice dell'applicazione. Un contenitore IoC costruisce gli oggetti per l'utente, che "inverte" il normale flusso di controllo.

Uso di contenitori IoC in SignalR

L'applicazione Chat è probabilmente troppo semplice da trarre vantaggio da un contenitore IoC. Esaminiamo invece l'esempio StockTicker .

L'esempio StockTicker definisce due classi principali:

  • StockTickerHub: classe hub, che gestisce le connessioni client.
  • StockTicker: un singleton che contiene i prezzi delle scorte e li aggiorna periodicamente.

StockTickerHubcontiene un riferimento al singleton, mentre StockTicker contiene un riferimento all'oggetto StockTickerIHubConnectionContext per StockTickerHub. Usa questa interfaccia per comunicare con StockTickerHub le istanze. Per altre informazioni, vedere Trasmissione server con ASP.NET SignalR.

È possibile usare un contenitore IoC per scollegare queste dipendenze un bit. Per prima cosa, semplificare le StockTickerHub classi e StockTicker . Nel codice seguente ho commentato le parti che non abbiamo bisogno.

Rimuovere il costruttore senza parametri da StockTicker. Verrà invece sempre usato DI per creare l'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;
    }

    // ...

Per StockTicker rimuovere l'istanza singleton. Successivamente si userà il contenitore IoC per controllare la durata di StockTicker. Rendere anche pubblico il costruttore.

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

Successivamente, è possibile eseguire il refactoring del codice creando un'interfaccia per StockTicker. Questa interfaccia verrà usata per separare l'oggetto StockTickerHub dalla StockTicker classe.

Visual Studio semplifica questo tipo di refactoring. Aprire il file StockTicker.cs, fare clic con il pulsante destro del mouse sulla dichiarazione di StockTicker classe e selezionare Refactor ... Estrai interfaccia.

Screenshot del menu a discesa clic con il pulsante destro del mouse visualizzato su Visual Studio Code, con le opzioni Refactor e Extract Interface evidenziate.

Nella finestra di dialogo Estrai interfaccia fare clic su Seleziona tutto. Lasciare invariate le altre impostazioni predefinite. Fare clic su OK.

Screenshot della finestra di dialogo Estrai interfaccia con l'opzione Seleziona tutto evidenziata e O K visualizzata.

Visual Studio crea una nuova interfaccia denominata IStockTickere cambia StockTicker anche per derivare da IStockTicker.

Aprire il file IStockTicker.cs e modificare l'interfaccia in pubblico.

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

StockTickerHub Nella classe modificare le due istanze di StockTicker in IStockTicker:

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

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

La creazione di un'interfaccia IStockTicker non è strettamente necessaria, ma ho voluto illustrare come DI può aiutare a ridurre l'accoppiamento tra i componenti nell'applicazione.

Aggiungere la libreria Ninject

Per .NET sono disponibili molti contenitori IoC open source. Per questa esercitazione, userò Ninject. Altre librerie popolari includono Castle Windsor, Spring.Net, Autofac, Unity e StructureMap.

Usare Gestione pacchetti NuGet per installare la libreria Ninject. In Visual Studio dal menu Strumenti selezionareConsole gestione pacchetti>NuGet. Nella finestra Console di gestione pacchetti immettere il comando seguente:

Install-Package Ninject -Version 3.0.1.10

Sostituire il resolver di dipendenza SignalR

Per usare Ninject all'interno di SignalR, creare una classe che deriva da 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));
    }
}

Questa classe esegue l'override dei metodi GetService e GetServices di DefaultDependencyResolver. SignalR chiama questi metodi per creare vari oggetti in fase di esecuzione, incluse le istanze dell'hub, nonché vari servizi usati internamente da SignalR.

  • Il metodo GetService crea una singola istanza di un tipo. Eseguire l'override di questo metodo per chiamare il metodo TryGet del kernel Ninject. Se il metodo restituisce Null, tornare al resolver predefinito.
  • Il metodo GetServices crea una raccolta di oggetti di un tipo specificato. Eseguire l'override di questo metodo per concatenare i risultati di Ninject con i risultati del resolver predefinito.

Configurare associazioni Ninject

A questo punto si userà Ninject per dichiarare associazioni di tipo.

Aprire il file RegisterHubs.cs. RegisterHubs.Start Nel metodo creare il contenitore Ninject, che Ninject chiama il kernel.

var kernel = new StandardKernel();

Creare un'istanza del sistema di risoluzione delle dipendenze personalizzato:

var resolver = new NinjectSignalRDependencyResolver(kernel);

Creare un'associazione per IStockTicker come indicato di seguito:

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

Questo codice sta dicendo due cose. Prima di tutto, ogni volta che l'applicazione necessita di , il kernel deve creare un'istanza IStockTickerdi StockTicker. In secondo luogo, la StockTicker classe deve essere creata come oggetto singleton. Ninject creerà un'istanza dell'oggetto e restituirà la stessa istanza per ogni richiesta.

Creare un'associazione per IHubConnectionContext come indicato di seguito:

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

Questo codice crea una funzione anonima che restituisce un IHubConnection. Il metodo WhenInjectInto indica a Ninject di usare questa funzione solo quando si creano IStockTicker istanze. Il motivo è che SignalR crea istanze IHubConnectionContext internamente e non si vuole eseguire l'override del modo in cui SignalR li crea. Questa funzione si applica solo alla StockTicker classe.

Passare il resolver di dipendenza al metodo MapHubs :

RouteTable.Routes.MapHubs(config);

SignalR userà ora il resolver specificato in MapHubs anziché il resolver predefinito.

Ecco l'elenco completo del codice per 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);
    }
}

Per eseguire l'applicazione StockTicker in Visual Studio, premere F5. Nella finestra del browser passare a http://localhost:*port*/SignalR.Sample/StockTicker.html.

Screenshot della schermata A S P dot NET Signal R Stock Ticker Sample che viene visualizzata in una finestra del browser Internet Explorer.

L'applicazione ha esattamente la stessa funzionalità di prima. Per una descrizione, vedere Trasmissione server con ASP.NET SignalR. Non è stato modificato il comportamento; è stato appena reso il codice più semplice da testare, gestire ed evolvere.