Внедрение зависимостей в SignalR 1.x

Патрик Флетчер

Предупреждение

Эта документация не подходит для последней версии SignalR. Ознакомьтесь с ASP.NET Core SignalR.

Внедрение зависимостей — это способ удаления жестко запрограммированных зависимостей между объектами, упрощающий замену зависимостей объекта либо для тестирования (с помощью макетных объектов), либо для изменения поведения во время выполнения. В этом руководстве показано, как выполнить внедрение зависимостей в концентраторах SignalR. В ней также показано, как использовать контейнеры IoC с SignalR. Контейнер IoC — это общая платформа для внедрения зависимостей.

Что такое внедрение зависимостей?

Пропустите этот раздел, если вы уже знакомы с внедрением зависимостей.

Внедрение зависимостей (DI) — это шаблон, в котором объекты не отвечают за создание собственных зависимостей. Вот простой пример для мотивации внедрения внедрения. Предположим, у вас есть объект , который должен регистрировать сообщения. Вы можете определить интерфейс ведения журнала:

interface ILogger 
{
    void LogMessage(string message);
}

В объекте можно создать для ILogger записи в журнал сообщений:

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

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

Это работает, но это не лучший дизайн. Если вы хотите заменить FileLogger другой ILogger реализацией, необходимо изменить SomeComponent. Предположим, что многие другие объекты используют FileLogger, вам потребуется изменить их все. Или если вы решили сделать FileLogger одноэлементный, вам также потребуется внести изменения во всем приложении.

Лучший подход заключается в том, чтобы "внедрить" ILogger в объект , например, с помощью аргумента конструктора:

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

Теперь объект не отвечает за выбор используемого ILogger . Вы можете переключать ILogger реализации, не изменяя зависимые от нее объекты.

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

Этот шаблон называется внедрением конструктора. Другой шаблон — внедрение метода задания, при котором зависимость задается с помощью метода задания или свойства.

Простое внедрение зависимостей в SignalR

Рассмотрим приложение Chat из руководства начало работы с SignalR. Ниже приведен класс концентратора из этого приложения:

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

Предположим, что вы хотите сохранить сообщения чата на сервере перед их отправкой. Можно определить интерфейс, который абстрагирует эту функцию, и использовать внедрение интерфейса для внедрения интерфейса в 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);
    }

Единственная проблема заключается в том, что приложение SignalR не создает концентраторы напрямую. SignalR создает их для вас. По умолчанию SignalR ожидает, что класс концентратора будет иметь конструктор без параметров. Однако можно легко зарегистрировать функцию для создания экземпляров концентратора и использовать эту функцию для выполнения внедрения внедрения. Зарегистрируйте функцию, вызвав GlobalHost.DependencyResolver.Register.

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

    RouteTable.Routes.MapHubs();

    // ...
}

Теперь SignalR будет вызывать эту анонимную функцию всякий ChatHub раз, когда ей нужно создать экземпляр.

Контейнеры IoC

Предыдущий код подходит для простых случаев. Но вам все равно пришлось написать следующее:

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

В сложном приложении с множеством зависимостей может потребоваться написать много такого "проводного" кода. Этот код может быть трудно поддерживать, особенно если зависимости являются вложенными. Кроме того, сложно выполнить модульный тест.

Одним из решений является использование контейнера IoC. Контейнер IoC — это программный компонент, отвечающий за управление зависимостями. Вы регистрируете типы в контейнере, а затем используете контейнер для создания объектов. Контейнер автоматически определяет отношения зависимостей. Многие контейнеры IoC также позволяют управлять временем существования объектов и область.

Примечание

IoC означает инверсию управления, что является общим шаблоном, в котором платформа вызывает код приложения. Контейнер IoC создает объекты за вас, что "инвертирует" обычный поток управления.

Использование контейнеров IoC в SignalR

Приложение Chat, вероятно, слишком просто, чтобы воспользоваться преимуществами контейнера IoC. Вместо этого давайте рассмотрим пример StockTicker .

Пример StockTicker определяет два класса main:

  • StockTickerHub: класс концентратора, который управляет клиентскими подключениями.
  • StockTicker: одноэлементный объект, который держит цены на акции и периодически обновляет их.

StockTickerHub содержит ссылку на одноэлементный StockTicker объект , а StockTicker содержит ссылку на IHubConnectionContext для StockTickerHub. Он использует этот интерфейс для взаимодействия с StockTickerHub экземплярами. (Дополнительные сведения см. в разделе Широковещательная трансляция сервера с помощью ASP.NET SignalR.)

Мы можем использовать контейнер IoC, чтобы немного распутать эти зависимости. Сначала упростим классы StockTickerHub и StockTicker . В следующем коде я закомментировал части, которые нам не нужны.

Удалите конструктор без параметров из StockTicker. Вместо этого мы всегда будем использовать внедрение зависимостей для создания концентратора.

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

    // ...

Для StockTicker удалите одноэлементный экземпляр. Позже мы будем использовать контейнер IoC для управления временем существования StockTicker. Кроме того, сделайте конструктор общедоступным.

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

Затем можно выполнить рефакторинг кода, создав интерфейс для StockTicker. Мы будем использовать этот интерфейс для отделения StockTickerHub от StockTicker класса .

Visual Studio упрощает такой рефакторинг. Откройте файл StockTicker.cs, щелкните правой StockTicker кнопкой мыши объявление класса и выберите Рефакторинг ... Извлечение интерфейса.

Снимок экрана: раскрывающееся меню с щелчком правой кнопкой мыши, отображающее Visual Studio Code с выделенными параметрами Рефакторинг и Извлечь интерфейс.

В диалоговом окне Извлечение интерфейса щелкните Выбрать все. Оставьте другие значения по умолчанию. Нажмите кнопку ОК.

Снимок экрана: диалоговое окно

Visual Studio создает новый интерфейс с именем IStockTicker, а также изменяет StockTicker наследование от IStockTicker.

Откройте файл IStockTicker.cs и измените интерфейс на общедоступный.

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

StockTickerHub В классе измените два экземпляра на StockTickerIStockTicker:

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

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

IStockTicker Создание интерфейса не является строго обязательным, но я хотел бы показать, как внедрение внедрения зависимостей может помочь уменьшить взаимозависимость между компонентами в приложении.

Добавление библиотеки Ninject

Существует множество контейнеров IoC с открытым кодом для .NET. В этом руководстве мы будем использовать Ninject. (Другие популярные библиотеки включают Castle Windsor, Spring.Net, Autofac, Unity и StructureMap.)

Используйте диспетчер пакетов NuGet для установки библиотеки Ninject. В Visual Studio в меню Сервис выберитеКонсоль диспетчера пакетов>NuGet. В окне "Консоль диспетчера пакетов" введите следующую команду:

Install-Package Ninject -Version 3.0.1.10

Замена сопоставителя зависимостей SignalR

Чтобы использовать Ninject в SignalR, создайте класс, производный от 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));
    }
}

Этот класс переопределяет методы GetService и GetServicesкласса DefaultDependencyResolver. SignalR вызывает эти методы для создания различных объектов во время выполнения, включая экземпляры концентратора, а также различные службы, используемые SignalR внутри системы.

  • Метод GetService создает один экземпляр типа . Переопределите этот метод, чтобы вызвать метод TryGet ядра Ninject. Если этот метод возвращает значение NULL, вернитесь к сопоставительу по умолчанию.
  • Метод GetServices создает коллекцию объектов указанного типа. Переопределите этот метод, чтобы объединить результаты из Ninject с результатами сопоставителя по умолчанию.

Настройка привязок Ninject

Теперь мы будем использовать Ninject для объявления привязок типов.

Откройте файл RegisterHubs.cs. В методе RegisterHubs.Start создайте контейнер Ninject, который Ninject вызывает ядро.

var kernel = new StandardKernel();

Создайте экземпляр пользовательского сопоставителя зависимостей:

var resolver = new NinjectSignalRDependencyResolver(kernel);

Создайте привязку для IStockTicker следующим образом:

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

Этот код говорит о двух вещах. Во-первых, когда приложению требуется IStockTicker, ядро должно создать экземпляр StockTicker. Во-вторых StockTicker , класс должен быть создан как одноэлементный объект. Ninject создаст один экземпляр объекта и вернет один и тот же экземпляр для каждого запроса.

Создайте привязку для IHubConnectionContext следующим образом:

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

Этот код создает анонимную функцию, которая возвращает IHubConnection. Метод WhenInjectedInto указывает Ninject использовать эту функцию только при создании IStockTicker экземпляров. Причина в том, что SignalR создает экземпляры IHubConnectionContext внутренне, и мы не хотим переопределять, как SignalR создает их. Эта функция применяется только к нашему классу StockTicker .

Передайте сопоставитель зависимостей в метод MapHubs :

RouteTable.Routes.MapHubs(config);

Теперь SignalR будет использовать сопоставитель, указанный в MapHubs, а не сопоставитель по умолчанию.

Ниже приведен полный список кода для 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);
    }
}

Чтобы запустить приложение StockTicker в Visual Studio, нажмите клавишу F5. В окне браузера перейдите по адресу http://localhost:*port*/SignalR.Sample/StockTicker.html.

Снимок экрана: экран a S P dot NET Signal R Stock Ticker Sample (Пример) в окне браузера интернет-Обозреватель.

Приложение имеет те же функции, что и раньше. (Описание см. в разделе Широковещательная трансляция сервера с ASP.NET SignalR.) Мы не изменили поведение; просто упрощает тестирование, обслуживание и развитие кода.