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

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

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

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

Версии программного обеспечения, используемые в этом разделе

Предыдущие версии этого раздела

Сведения о более ранних версиях SignalR см. в разделе Старые версии SignalR.

Вопросы и комментарии

Оставьте отзыв о том, как вам понравилось это руководство и что мы могли бы улучшить в комментариях в нижней части страницы. Если у вас есть вопросы, которые не связаны напрямую с руководством, вы можете опубликовать их на форуме ASP.NET SignalR или StackOverflow.com.

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

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

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

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

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

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

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

    App.MapSignalR();

    // ...
}

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

Контейнеры IoC

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

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

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

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

Примечание

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

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

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

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

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

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

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

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

[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<dynamic> 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 два экземпляра StockTicker на IStockTicker:

[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 и GetServicesdefaultDependencyResolver. SignalR вызывает эти методы для создания различных объектов во время выполнения, включая экземпляры концентратора, а также различные службы, используемые SignalR внутри системы.

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

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

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

Откройте класс Startup.cs приложения (который вы создали вручную согласно инструкциям по пакету в readme.txt, или который был создан путем добавления проверки подлинности в проект). В методе Startup.Configuration создайте контейнер 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(typeof(IHubConnectionContext<dynamic>)).ToMethod(context =>
                    resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
                     ).WhenInjectedInto<IStockTicker>();

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

Передайте сопоставитель зависимостей в метод MapSignalR , добавив конфигурацию концентратора:

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

Обновите метод Startup.ConfigureSignalR в классе Startup примера с помощью нового параметра:

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

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

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

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

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

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