Udostępnij za pośrednictwem


Wstrzykiwanie zależności w usłudze SignalR 1.x

Autor : Patrick Fletcher

Ostrzeżenie

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

Wstrzykiwanie zależności to sposób usuwania zakodowanych zależności między obiektami, co ułatwia zastępowanie zależności obiektu na potrzeby testowania (przy użyciu pozorowanych obiektów) lub zmiany zachowania w czasie wykonywania. W tym samouczku pokazano, jak wykonać wstrzykiwanie zależności w centrach SignalR. Pokazano również, jak używać kontenerów IoC z usługą SignalR. Kontener IoC to ogólna struktura iniekcji 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ć element 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 FileLoggermetody , należy zmienić wszystkie z nich. Lub jeśli zdecydujesz się wprowadzić FileLogger pojedynczą jedną aplikację, musisz również wprowadzić zmiany w całej aplikacji.

Lepszym podejściem 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 konieczności zmieniania obiektów, które od niego zależą.

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

Ten wzorzec jest nazywany iniekcją konstruktora. Inny wzorzec to iniekcja ustawiająca, gdzie zależność jest ustawiana za pomocą metody ustawiającej lub właściwości.

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

Rozważmy aplikację Czat z samouczka Wprowadzenie z usługą SignalR. 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ć wiadomości czatu na serwerze przed ich wysłaniem. Można zdefiniować interfejs, który abstrahuje tę funkcję, i użyć di do wstrzyknięcia 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 koncentratorów; Usługa SignalR tworzy je za Ciebie. Domyślnie usługa SignalR oczekuje, że klasa centrum 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 wykonywania di. Zarejestruj funkcję, wywołując funkcję GlobalHost.DependencyResolver.Register.

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

    RouteTable.Routes.MapHubs();

    // ...
}

Teraz usługa SignalR wywoła tę funkcję anonimową ChatHub za każdym razem, gdy będzie konieczne utworzenie wystąpienia.

Kontenery IoC

Poprzedni kod jest odpowiedni dla prostych przypadków. Ale nadal trzeba było to napisać:

... 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, który jest odpowiedzialny za zarządzanie zależnościami. Typy można zarejestrować w kontenerze, a następnie użyć kontenera do utworzenia 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 platforma wywołuje kod aplikacji. Kontener IoC konstruuje obiekty, co "odwraca" zwykły przepływ sterowania.

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

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

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

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

StockTickerHub zawiera 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 StockTickerHub wystąpieniami. (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 usunąć splątanie tych 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 StockTickerklasy . Zamiast tego zawsze użyjemy 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 przypadku stockticker usuń pojedyncze wystąpienie. Później użyjemy kontenera IoC do kontrolowania okresu istnienia StockTicker. Ustaw również 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 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 elementu StockTicker. Użyjemy tego interfejsu, aby rozdzielić element StockTickerHub 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 po kliknięciu prawym przyciskiem myszy wyświetlane na Visual Studio Code z wyróżnionymi opcjami Refaktoryzacja i wyodrębnianie interfejsu.

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ą Zaznacz wszystko i wyświetlaną opcją O K.

Program Visual Studio tworzy nowy interfejs o nazwie IStockTicker, a także zmiany StockTicker w celu uzyskania wartości .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();
}

StockTickerHub W klasie zmień dwa wystąpienia StockTicker klasy 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 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. W tym samouczku użyję narzędzia Ninject. (Inne popularne biblioteki to Castle Windsor, Spring.Net, Autofac, Unity i StructureMap.

Użyj Menedżera pakietów NuGet, aby zainstalować bibliotekę Ninject. W programie Visual Studio z menu Narzędzia wybierz pozycjęKonsola menedżera pakietówNuGet Package Manager>. W oknie Konsola menedżera pakietów wprowadź następujące polecenie:

Install-Package Ninject -Version 3.0.1.10

Zastępowanie programu SignalR Dependency Resolver

Aby użyć narzędzia Ninject w usłudze SignalR, utwórz klasę pochodzącą z elementu 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 GetServiceselementu DefaultDependencyResolver. Usługa SignalR wywołuje te metody w celu utworzenia różnych obiektów w czasie wykonywania, w tym wystąpień centrum, 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 programu rozpoznawania nazw.
  • Metoda GetServices tworzy kolekcję obiektów określonego typu. Zastąpij tę metodę, aby połączyć wyniki z programu Ninject z wynikami domyślnego programu rozpoznawania nazw.

Konfigurowanie powiązań Ninject

Teraz użyjemy narzędzia Ninject, aby zadeklarować powiązania typów.

Otwórz plik RegisterHubs.cs. W metodzie RegisterHubs.Start 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<IHubConnectionContext>().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 MapHubs :

RouteTable.Routes.MapHubs(config);

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

Oto pełna lista kodu dla RegisterHubs.Startelementu .

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

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 ekran przykładu S P dot NET Signal R Stock Ticker wyświetlany w oknie przeglądarki Internet Explorer.

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.