Wstrzykiwanie zależności w usłudze SignalR

Autor : Patrick Fletcher

Ostrzeżenie

Ta dokumentacja nie jest przeznaczona dla najnowszej wersji usługi SignalR. Przyjrzyj się ASP.NET Core SignalR.

Wersje oprogramowania używane w tym temacie

Poprzednie wersje tego tematu

Aby uzyskać informacje o wcześniejszych wersjach usługi SignalR, zobacz SignalR Older Versions (Starsze wersje usługi SignalR).

Pytania i komentarze

Prześlij opinię na temat tego, jak podoba ci się ten samouczek i co możemy poprawić w komentarzach w dolnej części strony. Jeśli masz pytania, które nie są bezpośrednio związane z samouczkiem, możesz opublikować je na forum ASP.NET SignalR lub StackOverflow.com.

Wstrzykiwanie zależności to sposób usuwania trwale zakodowanych zależności między obiektami, co ułatwia zamianę zależności obiektu na potrzeby testowania (przy użyciu makiety obiektów) lub zmiany zachowania czasu wykonywania. W tym samouczku pokazano, jak wykonać iniekcję zależności na koncentratorach usługi SignalR. Pokazano również, jak używać kontenerów IoC z usługą SignalR. Kontener IoC to ogólna struktura wstrzykiwania zależności.

Co to jest wstrzykiwanie zależności?

Pomiń tę sekcję, jeśli znasz już iniekcję zależności.

Wstrzykiwanie zależności (DI) to wzorzec, w którym obiekty nie są odpowiedzialne za tworzenie własnych zależności. Oto prosty przykład motywowania di. Załóżmy, że masz obiekt, który musi rejestrować komunikaty. Możesz zdefiniować interfejs rejestrowania:

interface ILogger 
{
    void LogMessage(string message);
}

W obiekcie można utworzyć obiekt do rejestrowania ILogger komunikatów:

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

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

To działa, ale nie jest to najlepszy projekt. Jeśli chcesz zastąpić FileLogger inną ILogger implementacją, musisz zmodyfikować SomeComponentelement . Załóżmy, że wiele innych obiektów używa metody FileLogger, trzeba będzie zmienić wszystkie z nich. Lub jeśli zdecydujesz się wprowadzić FileLogger pojedynczą aplikację, musisz również wprowadzić zmiany w całej aplikacji.

Lepszym rozwiązaniem jest "wstrzyknięcie" obiektu ILogger — na przykład przy użyciu argumentu konstruktora:

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

Teraz obiekt nie jest odpowiedzialny za wybór, którego ILogger należy użyć. Implementacje można przełączać ILogger bez zmieniania obiektów, które są od niego zależne.

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

Ten wzorzec jest nazywany wstrzyknięciem konstruktora. Inny wzorzec to iniekcja ustawiania, w którym zależność jest ustawiana za pomocą metody lub właściwości setter.

Proste wstrzykiwanie zależności w usłudze SignalR

Rozważ aplikację Czat z samouczka Wprowadzenie za pomocą usługi SignalR. Oto klasa koncentratora z tej aplikacji:

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

Załóżmy, że chcesz przechowywać wiadomości czatu na serwerze przed ich wysłaniem. Możesz zdefiniować interfejs, który wyodrębnia tę funkcję, i użyć di do wstrzykiwania interfejsu ChatHub do klasy.

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

Jedynym problemem jest to, że aplikacja SignalR nie tworzy bezpośrednio centrów; Usługa SignalR tworzy je dla Ciebie. Domyślnie usługa SignalR oczekuje, że klasa piasty będzie miała konstruktor bez parametrów. Można jednak łatwo zarejestrować funkcję w celu utworzenia wystąpień centrum i użyć tej funkcji do wykonania di. Zarejestruj funkcję, wywołując polecenie GlobalHost.DependencyResolver.Register.

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

    App.MapSignalR();

    // ...
}

Teraz usługa SignalR wywoła tę funkcję anonimową ChatHub za każdym razem, gdy musi utworzyć wystąpienie.

Kontenery IoC

Poprzedni kod jest w porządku w prostych przypadkach. Ale nadal trzeba było napisać to:

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

W złożonej aplikacji z wieloma zależnościami może być konieczne napisanie wielu kodu "okablowania". Ten kod może być trudny do utrzymania, zwłaszcza jeśli zależności są zagnieżdżone. Trudno jest również przeprowadzić test jednostkowy.

Jednym z rozwiązań jest użycie kontenera IoC. Kontener IoC to składnik oprogramowania odpowiedzialny za zarządzanie zależnościami. Rejestrujesz typy w kontenerze, a następnie używasz kontenera do tworzenia obiektów. Kontener automatycznie określa relacje zależności. Wiele kontenerów IoC umożliwia również kontrolowanie elementów, takich jak okres istnienia obiektu i zakres.

Uwaga

"IoC" oznacza "inversion of control", czyli ogólny wzorzec, w którym struktura wywołuje kod aplikacji. Kontener IoC tworzy obiekty dla Ciebie, co "odwraca" zwykły przepływ sterowania.

Używanie kontenerów IoC w usłudze SignalR

Aplikacja Czat jest prawdopodobnie zbyt prosta, aby skorzystać z kontenera IoC. Zamiast tego przyjrzyjmy się przykładowi StockTicker .

Przykład StockTicker definiuje dwie główne klasy:

  • StockTickerHub: klasa piasty, która zarządza połączeniami klienta.
  • StockTicker: Singleton, który posiada ceny akcji i okresowo je aktualizuje.

StockTickerHub przechowuje odwołanie do pojedynczego StockTicker elementu, a element StockTicker zawiera odwołanie do elementu IHubConnectionContext dla elementu StockTickerHub. Używa tego interfejsu do komunikowania się z wystąpieniami StockTickerHub . (Aby uzyskać więcej informacji, zobacz Server Broadcast with ASP.NET SignalR(Emisja serwera za pomocą usługi SignalR ASP.NET).

Możemy użyć kontenera IoC, aby nieco rozplątać te zależności. Najpierw uprościmy StockTickerHub klasy i StockTicker . W poniższym kodzie skomentowałem części, których nie potrzebujemy.

Usuń konstruktor bez parametrów z StockTickerHub. Zamiast tego zawsze będziemy używać di do utworzenia koncentratora.

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

    // ...

W polu StockTicker usuń pojedyncze wystąpienie. Później użyjemy kontenera IoC do kontrolowania okresu istnienia usługi StockTicker. Ponadto ustaw konstruktor jako publiczny.

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

Następnie możemy refaktoryzować kod, tworząc interfejs dla StockTickerelementu . Użyjemy tego interfejsu, aby rozdzielić StockTickerHub element z StockTicker klasy .

Program Visual Studio ułatwia refaktoryzację tego rodzaju. Otwórz plik StockTicker.cs, kliknij prawym przyciskiem myszy deklarację StockTicker klasy i wybierz pozycję Refaktoryzacja ... Wyodrębnij interfejs.

Zrzut ekranu przedstawiający menu rozwijane kliknięcie prawym przyciskiem myszy w programie Visual Studio code z wyróżnionymi opcjami Refraktor i Wyodrębnij interfejs.

W oknie dialogowym Wyodrębnij interfejs kliknij pozycję Wybierz wszystko. Inne wartości pozostaw domyślne. Kliknij przycisk OK.

Zrzut ekranu przedstawiający okno dialogowe Wyodrębnianie interfejsu z wyróżnioną opcją Wybierz wszystko z zaznaczonymi wszystkimi dostępnymi opcjami.

Program Visual Studio tworzy nowy interfejs o nazwie IStockTicker, a także zmiany StockTicker wynikające z IStockTickerelementu .

Otwórz plik IStockTicker.cs i zmień interfejs na publiczny.

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

StockTickerHub W klasie zmień dwa wystąpienia na StockTickerIStockTicker:

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

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

Tworzenie interfejsu IStockTicker nie jest absolutnie konieczne, ale chciałem pokazać, jak di może pomóc zmniejszyć sprzężenie między składnikami w aplikacji.

Dodawanie biblioteki Ninject

Istnieje wiele kontenerów IoC typu open source dla platformy .NET. Na potrzeby tego samouczka użyję ninject. (Inne popularne biblioteki to Castle Windsor, Spring.Net, Autofac, Unity i StructureMap.

Zainstaluj bibliotekę Ninject za pomocą Menedżera pakietów NuGet. W programie Visual Studio z menu Narzędzia wybierz pozycjęKonsola Menedżera pakietówNuGet Menedżera> pakietów. W oknie Konsola menedżera pakietów wprowadź następujące polecenie:

Install-Package Ninject -Version 3.0.1.10

Zastąp program SignalR Dependency Resolver

Aby użyć programu Ninject w usłudze SignalR, utwórz klasę pochodzącą z 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));
    }
}

Ta klasa zastępuje metody GetService i GetServicesdefaultDependencyResolver. Usługa SignalR wywołuje te metody w celu utworzenia różnych obiektów w czasie wykonywania, w tym wystąpień koncentratora, a także różnych usług używanych wewnętrznie przez usługę SignalR.

  • Metoda GetService tworzy pojedyncze wystąpienie typu. Zastąpij tę metodę, aby wywołać metodę TryGet jądra Ninject. Jeśli ta metoda zwraca wartość null, wróć do domyślnego narzędzia rozpoznawania.
  • Metoda GetServices tworzy kolekcję obiektów określonego typu. Zastąpij tę metodę, aby połączyć wyniki z ninject z wynikami z domyślnego narzędzia rozpoznawania.

Konfigurowanie powiązań ninject

Teraz za pomocą narzędzia Ninject zadeklarujemy powiązania typów.

Otwórz klasę Startup.cs aplikacji (utworzoną ręcznie zgodnie z instrukcjami pakietu w pliku readme.txtlub utworzoną przez dodanie uwierzytelniania do projektu). W metodzie Startup.Configuration utwórz kontener Ninject, który Ninject wywołuje jądro.

var kernel = new StandardKernel();

Utwórz wystąpienie naszego niestandardowego narzędzia do rozpoznawania zależności:

var resolver = new NinjectSignalRDependencyResolver(kernel);

Utwórz powiązanie w IStockTicker następujący sposób:

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

Ten kod mówi dwie rzeczy. Najpierw zawsze, gdy aplikacja potrzebuje klasy IStockTicker, jądro powinno utworzyć wystąpienie klasy StockTicker. Po drugie, StockTicker klasa powinna być utworzona jako pojedynczy obiekt. Ninject utworzy jedno wystąpienie obiektu i zwróci to samo wystąpienie dla każdego żądania.

Utwórz powiązanie dla elementu IHubConnectionContext w następujący sposób:

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

Ten kod tworzy funkcję anonimową, która zwraca element IHubConnection. Metoda WhenInjectedInto informuje Ninject , aby używała tej funkcji tylko podczas tworzenia IStockTicker wystąpień. Przyczyną jest to, że usługa SignalR tworzy wystąpienia IHubConnectionContext wewnętrznie i nie chcemy zastępować sposobu tworzenia ich przez usługę SignalR. Ta funkcja ma zastosowanie tylko do naszej StockTicker klasy.

Przekaż program rozpoznawania zależności do metody MapSignalR , dodając konfigurację koncentratora:

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

Zaktualizuj metodę Startup.ConfigureSignalR w klasie Startup przykładu przy użyciu nowego parametru:

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

Teraz usługa SignalR użyje narzędzia rozpoznawania określonego w usłudze MapSignalR zamiast domyślnego narzędzia rozpoznawania.

Oto pełna lista kodu dla Startup.Configurationelementu .

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

Aby uruchomić aplikację StockTicker w programie Visual Studio, naciśnij klawisz F5. W oknie przeglądarki przejdź do http://localhost:*port*/SignalR.Sample/StockTicker.htmladresu .

Zrzut ekranu przedstawiający okno przeglądarki Internet Explorer z wyświetloną stroną internetową przykładową przykładową wykresu giełdowego S P P.NET Signal R Stock Ticker.

Aplikacja ma dokładnie taką samą funkcjonalność jak poprzednio. (Aby uzyskać opis, zobacz Server Broadcast with ASP.NET SignalR(Emisja serwera za pomocą usługi SignalR ASP.NET). Nie zmieniliśmy zachowania; właśnie ułatwił testowanie, konserwację i rozwijanie kodu.