Abhängigkeitsinjektion in SignalR

von Patrick Fletcher

Warnung

Diese Dokumentation ist nicht für die neueste Version von SignalR vorgesehen. Sehen Sie sich ASP.NET Core SignalR an.

In diesem Thema verwendete Softwareversionen

Frühere Versionen dieses Themas

Informationen zu früheren Versionen von SignalR finden Sie unter Ältere Versionen von SignalR.

Fragen und Kommentare

Bitte hinterlassen Sie Feedback darüber, wie Ihnen dieses Tutorial gefallen hat und was wir in den Kommentaren unten auf der Seite verbessern könnten. Wenn Sie Fragen haben, die sich nicht direkt auf das Tutorial beziehen, können Sie diese im ASP.NET SignalR-Forum oder im StackOverflow.com posten.

Abhängigkeitsinjektion ist eine Möglichkeit, hartcodierte Abhängigkeiten zwischen Objekten zu entfernen, wodurch das Ersetzen der Abhängigkeiten eines Objekts erleichtert wird, entweder zum Testen (mit Pseudoobjekten) oder zum Ändern des Laufzeitverhaltens. In diesem Tutorial erfahren Sie, wie Sie die Abhängigkeitsinjektion für SignalR-Hubs durchführen. Außerdem wird gezeigt, wie IoC-Container mit SignalR verwendet werden. Ein IoC-Container ist ein allgemeines Framework für die Abhängigkeitsinjektion.

Was ist Dependency Injection?

Überspringen Sie diesen Abschnitt, wenn Sie bereits mit der Abhängigkeitsinjektion vertraut sind.

Dependency Injection (DI) ist ein Muster, bei dem Objekte nicht für das Erstellen eigener Abhängigkeiten verantwortlich sind. Hier ist ein einfaches Beispiel, um DI zu motivieren. Angenommen, Sie verfügen über ein -Objekt, das Meldungen protokollieren muss. Sie können eine Protokollierungsschnittstelle definieren:

interface ILogger 
{
    void LogMessage(string message);
}

In Ihrem -Objekt können Sie eine ILogger erstellen, um Meldungen zu protokollieren:

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

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

Das funktioniert, aber es ist nicht das beste Design. Wenn Sie durch eine andere Implementierung ersetzen FileLogger möchten, müssen Sie ändernSomeComponent.ILogger Angenommen, viele andere Objekte verwenden FileLogger, müssen Sie alle ändern. Wenn Sie sich für ein Singleton entscheiden FileLogger , müssen Sie auch Änderungen in der gesamten Anwendung vornehmen.

Ein besserer Ansatz besteht darin, eine ILogger in das Objekt einzufügen, z. B. mithilfe eines Konstruktorarguments:

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

Jetzt ist das -Objekt nicht für die Auswahl der zu verwendenden ILogger Objekte verantwortlich. Sie können Implementierungen wechseln ILogger , ohne die Objekte zu ändern, die davon abhängen.

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

Dieses Muster wird als Konstruktorinjektion bezeichnet. Ein weiteres Muster ist die Setterinjektion, bei der Sie die Abhängigkeit über eine Settermethode oder -eigenschaft festlegen.

Einfache Abhängigkeitsinjektion in SignalR

Betrachten Sie die Chatanwendung aus dem Tutorial Erste Schritte mit SignalR. Dies ist die Hubklasse aus dieser Anwendung:

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

Angenommen, Sie möchten Chatnachrichten vor dem Senden auf dem Server speichern. Sie können eine Schnittstelle definieren, die diese Funktionalität abstrahiert, und di verwenden, um die Schnittstelle in die ChatHub -Klasse einzufügen.

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

Das einzige Problem ist, dass eine SignalR-Anwendung nicht direkt Hubs erstellt. SignalR erstellt sie für Sie. Standardmäßig erwartet SignalR, dass eine Hubklasse über einen parameterlosen Konstruktor verfügt. Sie können jedoch ganz einfach eine Funktion registrieren, um Hubinstanzen zu erstellen, und diese Funktion verwenden, um di auszuführen. Registrieren Sie die Funktion, indem Sie GlobalHost.DependencyResolver.Register aufrufen.

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

    App.MapSignalR();

    // ...
}

Jetzt ruft SignalR diese anonyme Funktion auf, wenn eine ChatHub instance erstellt werden muss.

IoC-Container

Der vorherige Code ist für einfache Fälle in Ordnung. Aber Sie mussten trotzdem folgendes schreiben:

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

In einer komplexen Anwendung mit vielen Abhängigkeiten müssen Sie möglicherweise einen Großteil dieses Verdrahtungscodes schreiben. Dieser Code kann schwer zu verwalten sein, insbesondere wenn Abhängigkeiten geschachtelt sind. Es ist auch schwierig, Komponententests zu testen.

Eine Lösung besteht darin, einen IoC-Container zu verwenden. Ein IoC-Container ist eine Softwarekomponente, die für die Verwaltung von Abhängigkeiten zuständig ist. Sie registrieren Typen beim Container und verwenden dann den Container, um Objekte zu erstellen. Der Container ermittelt automatisch die Abhängigkeitsbeziehungen. In vielen IoC-Containern können Sie auch Dinge wie Die Lebensdauer und den Gültigkeitsbereich von Objekten steuern.

Hinweis

"IoC" steht für "Inversion der Steuerung", ein allgemeines Muster, bei dem ein Framework Anwendungscode aufruft. Ein IoC-Container erstellt Ihre Objekte für Sie, wodurch der übliche Ablauf der Steuerung "invertiert" wird.

Verwenden von IoC-Containern in SignalR

Die Chatanwendung ist wahrscheinlich zu einfach, um von einem IoC-Container zu profitieren. Sehen wir uns stattdessen das StockTicker-Beispiel an.

Im StockTicker-Beispiel werden zwei Standard Klassen definiert:

  • StockTickerHub: Die Hubklasse, die Clientverbindungen verwaltet.
  • StockTicker: Ein Singleton, der Aktienkurse hält und diese regelmäßig aktualisiert.

StockTickerHub enthält einen Verweis auf das StockTicker Singleton, während StockTicker einen Verweis auf IHubConnectionContext für enthält StockTickerHub. Diese Schnittstelle wird für die Kommunikation mit StockTickerHub Instanzen verwendet. (Weitere Informationen finden Sie unter Server broadcast with ASP.NET SignalR.)

Wir können einen IoC-Container verwenden, um diese Abhängigkeiten ein wenig zu entwirren. Zunächst vereinfachen wir die StockTickerHub Klassen und StockTicker . Im folgenden Code habe ich die Teile auskommentiert, die wir nicht benötigen.

Entfernen Sie den parameterlosen Konstruktor aus StockTickerHub. Stattdessen verwenden wir immer di, um den Hub zu erstellen.

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

    // ...

Entfernen Sie für StockTicker die Singleton-instance. Später verwenden wir den IoC-Container, um die StockTicker-Lebensdauer zu steuern. Machen Sie den Konstruktor außerdem öffentlich.

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

Als Nächstes können wir den Code umgestalten, indem wir eine Schnittstelle für StockTickererstellen. Wir verwenden diese Schnittstelle, um die von der StockTicker -Klasse zu entkoppelnStockTickerHub.

Visual Studio erleichtert diese Art des Refactorings. Öffnen Sie die Datei StockTicker.cs, klicken Sie mit der rechten Maustaste auf die StockTicker Klassendeklaration, und wählen Sie Refactor ... Schnittstelle extrahieren.

Screenshot des Dropdownmenüs mit der rechten Maustaste über Visual Studio-Code mit hervorgehobenen Optionen refraktor und Schnittstelle extrahieren

Klicken Sie im Dialogfeld Schnittstelle extrahieren auf Alle auswählen. Behalten Sie die restlichen Standardwerte bei. Klicken Sie auf OK.

Screenshot des Dialogfelds

Visual Studio erstellt eine neue Schnittstelle mit dem Namen IStockTickerund änderungen StockTicker , um von IStockTickerabzuleiten.

Öffnen Sie die Datei IStockTicker.cs, und ändern Sie die Schnittstelle in public.

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

Ändern Sie in der StockTickerHub -Klasse die beiden Instanzen von 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;
    }

Das Erstellen einer IStockTicker Schnittstelle ist nicht unbedingt erforderlich, aber ich wollte zeigen, wie di helfen kann, die Kopplung zwischen Komponenten in Ihrer Anwendung zu reduzieren.

Hinzufügen der Ninject-Bibliothek

Es gibt viele Open-Source-IoC-Container für .NET. In diesem Tutorial verwende ich Ninject. (Weitere beliebte Bibliotheken sind Castle Windsor, Spring.Net, Autofac, Unity und StructureMap.)

Verwenden Sie den NuGet-Paket-Manager, um die Ninject-Bibliothek zu installieren. Wählen Sie in Visual Studio im Menü Extras die Option NuGet-Paket-Manager-Paket-Manager-Konsole> aus. Geben Sie im Fenster Paket-Manager-Konsole den folgenden Befehl ein:

Install-Package Ninject -Version 3.0.1.10

Ersetzen des SignalR-Abhängigkeitslöser

Um Ninject in SignalR zu verwenden, erstellen Sie eine Klasse, die von DefaultDependencyResolver abgeleitet ist.

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

Diese Klasse überschreibt die GetService - und GetServices-Methoden von DefaultDependencyResolver. SignalR ruft diese Methoden auf, um verschiedene Objekte zur Laufzeit zu erstellen, einschließlich Hubinstanzen, sowie verschiedene Dienste, die intern von SignalR verwendet werden.

  • Die GetService-Methode erstellt eine einzelne instance eines Typs. Überschreiben Sie diese Methode, um die TryGet-Methode des Ninject-Kernels aufzurufen. Wenn diese Methode NULL zurückgibt, greifen Sie auf den Standardlöser zurück.
  • Die GetServices-Methode erstellt eine Auflistung von Objekten eines angegebenen Typs. Überschreiben Sie diese Methode, um die Ergebnisse von Ninject mit den Ergebnissen des Standardlösers zu verketten.

Konfigurieren von Ninject-Bindungen

Nun verwenden wir Ninject, um Typbindungen zu deklarieren.

Öffnen Sie die Startup.cs-Klasse Ihrer Anwendung (die Sie entweder manuell gemäß den Paketanweisungen in readme.txterstellt haben oder die durch Hinzufügen der Authentifizierung zu Ihrem Projekt erstellt wurde). Erstellen Sie in der Startup.Configuration -Methode den Ninject-Container, der von Ninject den Kernel aufruft.

var kernel = new StandardKernel();

Erstellen Sie eine instance unseres benutzerdefinierten Abhängigkeitslösers:

var resolver = new NinjectSignalRDependencyResolver(kernel);

Erstellen Sie wie folgt eine Bindung für IStockTicker :

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

Dieser Code sagt zwei Dinge. Wenn die Anwendung einen IStockTickerbenötigt, sollte der Kernel zunächst eine instance von StockTickererstellen. Zweitens sollte es sich bei der StockTicker Klasse um ein als Singleton-Objekt erstelltes Objekt handeln. Ninject erstellt eine instance des Objekts und gibt für jede Anforderung denselben instance zurück.

Erstellen Sie wie folgt eine Bindung für IHubConnectionContext :

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

Dieser Code erstellt eine anonyme Funktion, die eine IHubConnection zurückgibt. Die WhenInjectedInto-Methode weist Ninject an, diese Funktion nur beim Erstellen von IStockTicker Instanzen zu verwenden. Der Grund ist, dass SignalR IHubConnectionContext-Instanzen intern erstellt, und wir möchten nicht überschreiben, wie SignalR sie erstellt. Diese Funktion gilt nur für unsere StockTicker Klasse.

Übergeben Sie den Abhängigkeitslöser an die MapSignalR-Methode , indem Sie eine Hubkonfiguration hinzufügen:

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

Aktualisieren Sie die Startup.ConfigureSignalR-Methode in der Startup-Klasse des Beispiels mit dem neuen Parameter:

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

SignalR verwendet nun den in MapSignalR angegebenen Resolver anstelle des Standardlösers.

Hier ist die vollständige Codeauflistung für 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);
    }
}

Drücken Sie F5, um die StockTicker-Anwendung in Visual Studio auszuführen. Navigieren Sie im Browserfenster zu http://localhost:*port*/SignalR.Sample/StockTicker.html.

Screenshot eines Internet Explorer Browserfensters mit der Webseite A S P dot NET Signal R Stock Ticker Sample

Die Anwendung verfügt über genau die gleiche Funktionalität wie zuvor. (Eine Beschreibung finden Sie unter Serverübertragung mit ASP.NET SignalR.) Wir haben das Verhalten nicht geändert. erleichterte das Testen, Verwalten und Weiterentwickeln des Codes.