Wstrzykiwanie zależności w usłudze SignalR

według Jan Wasson, Patryk Fletcher

Warning

Ta dokumentacja nie jest najnowsza dla najnowszej wersji usługi sygnalizującej. Zapoznaj się z tematem ASP.NET Core sygnalizujący.

Wersje oprogramowania używane w tym temacie

Poprzednie wersje tego tematu

Aby uzyskać informacje o wcześniejszych wersjach programu sygnalizującego, zobacz sekcję sygnalizujące starsze wersje.

Pytania i Komentarze

Prosimy o opinię na temat sposobu, w jaki lubię ten samouczek, i co możemy ulepszyć w komentarzach w dolnej części strony. Jeśli masz pytania, które nie są bezpośrednio związane z samouczkiem, możesz je ogłosić na forum ASP.NET lub StackOverflow.com.

Iniekcja zależności polega na usunięciu sztywnych zależności między obiektami, ułatwiając zastępowanie zależności obiektu, w przypadku testowania (przy użyciu obiektów makiety) lub zmiany zachowania w czasie wykonywania. Ten samouczek pokazuje, jak przeprowadzić iniekcję zależności w centrach sygnałów. Przedstawiono w nim również, jak używać kontenerów IoC z sygnalizacyjnym. Kontener IoC to ogólna struktura iniekcji zależności.

Co to jest iniekcja zależności?

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

Iniekcja zależności (di) jest wzorcem, w którym obiekty nie są odpowiedzialne za tworzenie własnych zależności. Oto prosty przykład do motywu DI. Załóżmy, że masz obiekt, który musi rejestrować komunikaty. Można zdefiniować interfejs rejestrowania:

interface ILogger 
{
    void LogMessage(string message);
}

W obiekcie można utworzyć ILogger do rejestrowania 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 najlepszym projektem. Jeśli chcesz zastąpić FileLogger inną implementacją ILogger, musisz zmodyfikować SomeComponent. Załóżmy, że wiele innych obiektów używa FileLogger, należy zmienić wszystkie z nich. Lub jeśli zdecydujesz się wprowadzić FileLogger pojedynczej, musisz również wprowadzić zmiany w całej aplikacji.

Lepszym rozwiązaniem jest "wstrzyknięcie" ILogger do obiektu — 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 odpowiada za wybór ILogger do użycia. Można zmienić implementacje ILogger bez zmiany 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 iniekcją konstruktora. Inny wzorzec to iniekcja setter, w której ustawia się zależność za pomocą metody ustawiającej lub właściwości.

Proste iniekcja zależności w sygnale

Rozważmy aplikację Chat z samouczka wprowadzenie za pomocą programu sygnalizującego. Oto Klasa centrum z tej aplikacji:

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

Załóżmy, że chcesz przechowywać komunikaty czatu na serwerze przed ich wysłaniem. Można zdefiniować interfejs, który jest abstrakcyjny dla tej funkcji, i użyć funkcji DI, aby wstrzyknąć interfejs do klasy ChatHub.

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

Jedyny problem polega na tym, że aplikacja sygnalizująca nie tworzy bezpośrednio centrów. Tworzy je dla Ciebie. Domyślnie program sygnalizujący oczekuje, że Klasa centrum ma Konstruktor bez parametrów. Można jednak łatwo zarejestrować funkcję do tworzenia wystąpień centrów i użyć tej funkcji do wykonania DI. Zarejestruj funkcję przez wywołanie GlobalHost. DependencyResolver. Register.

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

    App.MapSignalR();

    // ...
}

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

Kontenery IoC

Poprzedni kod jest bardzo drobny w przypadku prostych przypadków. Jednak nadal musiałeś napisać:

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

W złożonej aplikacji z wieloma zależnościami może być konieczne napisanie wielu tych kodów "okablowania". Ten kod może być trudny do utrzymania, szczególnie jeśli zależności są zagnieżdżone. Test jednostkowy jest również trudne.

Jednym z rozwiązań jest użycie kontenera IoC. Kontener IoC to składnik oprogramowania, który jest odpowiedzialny za zarządzanie zależnościami. Możesz zarejestrować typy w kontenerze, a następnie użyć kontenera do tworzenia obiektów. Kontener automatycznie określa relacje zależności. Wiele kontenerów IoC umożliwia również Sterowanie elementami, takimi jak okres istnienia obiektu i zakres.

Note

"IoC" oznacza "Inversion of Control", który jest ogólnym wzorcem, w którym struktura wywołuje kod aplikacji. Kontener IoC konstruuje obiekty, co oznacza, że jest to normalny przepływ sterowania.

Używanie kontenerów IoC w sygnalizacji

Aplikacja Chat jest prawdopodobnie zbyt prosta, aby można było skorzystać z kontenera IoC. Zamiast tego Przyjrzyjmy się przykładowi StockTicker .

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

  • StockTickerHub: Klasa Hub, która zarządza połączeniami klientów.
  • StockTicker: pojedynczy, który przechowuje ceny giełdowe, i okresowo aktualizuje je.

StockTickerHub przechowuje odwołanie do StockTicker pojedynczej, podczas gdy StockTicker przechowuje odwołanie do IHubConnectionContext dla StockTickerHub. Używa tego interfejsu do komunikowania się z wystąpieniami StockTickerHub. (Aby uzyskać więcej informacji, zobacz Server Broadcast with ASP.NET signaler).

Możemy użyć kontenera IoC, aby porządkowaniem te zależności jako bity. Najpierw uprośmy klasy StockTickerHub i StockTicker. W poniższym kodzie mam komentarz do niepotrzebnych elementów.

Usuń Konstruktor bez parametrów z StockTickerHub. Zamiast tego zawsze będziemy używać narzędzia DI do utworzenia centrum.

[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 przypadku StockTicker Usuń pojedyncze wystąpienie. Później będziemy używać kontenera IoC do kontrolowania okresu istnienia StockTicker. Ponadto ustaw konstruktora 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 zakodować kod przez utworzenie interfejsu dla StockTicker. Użyjemy tego interfejsu, aby rozdzielić StockTickerHub z klasy StockTicker.

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

W oknie dialogowym wyodrębnianie interfejsu kliknij pozycję Zaznacz wszystko. Pozostaw inne wartości domyślne. Kliknij przycisk OK.

Program Visual Studio tworzy nowy interfejs o nazwie IStockTicker, a także zmienia StockTicker na pochodny od IStockTicker.

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

W klasie StockTickerHub Zmień dwa wystąpienia StockTicker na IStockTicker:

[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 chcę pokazać, jak DI może pomóc w zmniejszeniu sprzęgania między składnikami w aplikacji.

Dodaj bibliotekę Ninject

Istnieje wiele kontenerów IoC "open source" dla platformy .NET. W tym samouczku użyjemy Ninject. (Inne popularne biblioteki obejmują Castle Windsor, Spring.NET, Autofac, Unityi StructureMap).

Zainstaluj bibliotekę Ninjectza pomocą Menedżera pakietów NuGet. W programie Visual Studio, w menu Narzędzia wybierz pozycję menedżer pakietów NuGet > konsola 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ępowanie programu rozpoznawania zależności sygnalizującego

Aby użyć Ninject w ramach sygnalizującego, Utwórz klasę, która pochodzi od 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 przesłania metody GetService i GetServices DefaultDependencyResolver. Sygnał wywołuje te metody, aby utworzyć różne obiekty w środowisku uruchomieniowym, w tym wystąpienia centrów, a także różne usługi używane wewnętrznie przez program sygnalizujący.

  • Metoda GetService tworzy pojedyncze wystąpienie typu. Zastąp tę metodę, aby wywołać metodę TryGet jądra Ninject. Jeśli ta metoda zwróci wartość null, powraca do domyślnego programu rozpoznawania nazw.
  • Metoda GetServices tworzy kolekcję obiektów określonego typu. Zastąp tę metodę, aby połączyć wyniki z Ninject z wynikami domyślnego programu rozpoznawania nazw.

Konfigurowanie powiązań Ninject

Teraz będziemy używać Ninject do deklarowania powiązań typów.

Otwórz klasę Startup.cs aplikacji (utworzoną ręcznie zgodnie z instrukcjami pakietu w 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 programu rozpoznawania zależności:

var resolver = new NinjectSignalRDependencyResolver(kernel);

Utwórz powiązanie dla IStockTicker w 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 zawiera dwie rzeczy. Po pierwsze, zawsze, gdy aplikacja wymaga IStockTicker, jądro powinno utworzyć wystąpienie StockTicker. Druga klasa StockTicker powinna być utworzona jako obiekt pojedynczy. Ninject utworzy jedno wystąpienie obiektu i zwróci to samo wystąpienie dla każdego żądania.

Utwórz powiązanie dla 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ą zwracającą IHubConnection. Metoda WhenInjectedInto informuje Ninject o użyciu tej funkcji tylko podczas tworzenia wystąpień IStockTicker. Przyczyną jest to, że sygnalizujący tworzy wewnętrznie wystąpienia IHubConnectionContext i nie chcemy przesłonić sposobu tworzenia przez program sygnalizującego. Ta funkcja ma zastosowanie tylko do naszej klasy StockTicker.

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

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

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

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

Teraz sygnalizujący będzie używać mechanizmu rozwiązywania konfliktów określonego w MapSignalRzamiast domyślnego programu rozpoznawania nazw.

Oto kompletna lista kodu dla 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);
    }
}

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

Aplikacja ma dokładnie te same funkcje jak wcześniej. (Aby uzyskać opis, zobacz Server Broadcast with ASP.NET signaler). Zachowanie nie zostało zmienione; znacznie ułatwiają testowanie, konserwację i rozwijanie kodu.