Abhängigkeitsinjektion in SignalR 1.x

von Patrick Fletcher

Warnung

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

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.

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

    RouteTable.Routes.MapHubs();

    // ...
}

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 StockTicker. 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 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, das über Visual Studio Code angezeigt wird, mit hervorgehobenen Optionen

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 zur Laufzeit verschiedene Objekte 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, wenden Sie sich 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

Jetzt verwenden wir Ninject, um Typbindungen zu deklarieren.

Öffnen Sie die Datei RegisterHubs.cs. Erstellen Sie in der RegisterHubs.Start -Methode den Ninject-Container, von dem Ninject den Kernel aufruft.

var kernel = new StandardKernel();

Erstellen Sie eine instance des 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 aus. Wenn die Anwendung einen IStockTickerbenötigt, sollte der Kernel zunächst eine instance von StockTickererstellen. Zweitens sollte die StockTicker -Klasse ein als Singleton-Objekt erstellt werden. Ninject erstellt eine instance des Objekts und gibt für jede Anforderung die gleiche instance zurück.

Erstellen Sie wie folgt eine Bindung für IHubConnectionContext :

kernel.Bind<IHubConnectionContext>().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ängigkeitsrelöser an die MapHubs-Methode :

RouteTable.Routes.MapHubs(config);

Jetzt verwendet SignalR den in MapHubs angegebenen Resolver anstelle des Standardrelösers.

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

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 des Bildschirms

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