Inserimento delle dipendenze in SignalR

di Patrick Fletcher

Avviso

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

Versioni software usate in questo argomento

Versioni precedenti di questo argomento

Per informazioni sulle versioni precedenti di SignalR, vedere Versioni precedenti di SignalR.

Domande e commenti

Lasciare commenti e suggerimenti su come è piaciuta questa esercitazione e cosa è possibile migliorare nei commenti nella parte inferiore della pagina. Se si hanno domande che non sono direttamente correlate all'esercitazione, è possibile pubblicarle nel forum di ASP.NET SignalR o StackOverflow.com.

L'inserimento delle dipendenze è un modo per rimuovere le dipendenze hardcoded tra gli 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 sugli hub SignalR. Illustra 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 l'inserimento delle dipendenze. 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 oggetto ILogger 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 FileLoggerusino , sarà necessario modificarli tutti. In alternativa, se si decide di apportare FileLogger un singleton, sarà necessario apportare modifiche in tutta l'applicazione.

Un approccio migliore consiste nell'inserire un oggetto ILogger 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 di quale ILogger 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 è denominato injection del costruttore. Un altro modello è l'inserimento di setter, 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 dell'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 l'inserimento dell'inserimento dell'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 automaticamente. Per impostazione predefinita, SignalR prevede che una classe hub abbia un costruttore senza parametri. Tuttavia, è possibile registrare facilmente una funzione per creare istanze hub e usare questa funzione per eseguire l'inserimento delle dipendenze. Registrare la funzione chiamando GlobalHost.DependencyResolver.Register.

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

    App.MapSignalR();

    // ...
}

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

Contenitori IoC

Il codice precedente va bene per i casi semplici. Ma hai ancora dovuto 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 sono annidate. È anche difficile eseguire unit test.

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

Nota

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

Uso di contenitori IoC in SignalR

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

L'esempio StockTicker definisce due classi principali:

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

StockTickerHubcontiene un riferimento al StockTicker singleton, mentre StockTicker contiene un riferimento a IHubConnectionContext 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 separare queste dipendenze un po'. In primo luogo, è necessario semplificare le StockTickerHub classi e StockTicker . Nel codice seguente ho commentato le parti che non abbiamo bisogno.

Rimuovere il costruttore senza parametri da StockTickerHub. Si userà invece l'inserimento delle dipendenze 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<dynamic> clients)
    {
        if (clients == null)
        {
            throw new ArgumentNullException("clients");
        }

        Clients = clients;
        LoadDefaultStocks();
    }

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

È quindi possibile effettuare il refactoring del codice creando un'interfaccia per StockTicker. Questa interfaccia verrà usata per separare la StockTickerHub classe 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 scegliere Refactoring ... Estrai interfaccia.

Screenshot del menu a discesa con il pulsante destro del mouse su Visual Studio Code con le opzioni Refractor 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, con tutte le opzioni disponibili selezionate.

Visual Studio crea una nuova interfaccia denominata IStockTickere modifica 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 volevo mostrare in che modo l'inserimento delle dipendenze può contribuire a ridurre l'accoppiamento tra i componenti nell'applicazione.

Aggiungere la libreria Ninject

Esistono molti contenitori IoC open source per .NET. 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 scegliere Console diGestione>pacchetti NuGetdal menu Strumenti. Nella finestra Console di gestione pacchetti immettere il comando seguente:

Install-Package Ninject -Version 3.0.1.10

Sostituire il resolver di dipendenze 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, eseguire il fallback 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 sistema di risoluzione predefinito.

Configurare le associazioni Ninject

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

Aprire la classe Startup.cs dell'applicazione (creata manualmente in base alle istruzioni del pacchetto in readme.txto creata aggiungendo l'autenticazione al progetto). Startup.Configuration 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 richiede un IStockTicker, il kernel deve creare un'istanza di 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(typeof(IHubConnectionContext<dynamic>)).ToMethod(context =>
                    resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
                     ).WhenInjectedInto<IStockTicker>();

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

Passare il resolver di dipendenze al metodo MapSignalR aggiungendo una configurazione hub:

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

Aggiornare il metodo Startup.ConfigureSignalR nella classe Startup dell'esempio con il nuovo parametro:

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

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

Di seguito è riportato il listato di codice completo per 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);
    }
}

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

Screenshot di una finestra del browser Internet Explorer, che mostra la pagina Web A S P dot NET Signal R Stock Ticker Sample.

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