SignalR 1.x 中的依赖项注入

作者 :Patrick Fletcher

警告

本文档不适用于最新版本的 SignalR。 查看 ASP.NET Core SignalR

依赖关系注入是一种删除对象之间的硬编码依赖项的方法,可以更轻松地替换对象的依赖项,以便使用 mock 对象) 测试 (或更改运行时行为。 本教程介绍如何在 SignalR 中心上执行依赖项注入。 它还演示如何将 IoC 容器与 SignalR 配合使用。 IoC 容器是依赖关系注入的常规框架。

什么是依赖关系注入?

如果已熟悉依赖项注入,请跳过此部分。

依赖项注入 (DI) 是一种模式,其中对象不负责创建自己的依赖项。 下面是激励 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);

此模式称为 构造函数注入。 另一种模式是 setter 注入,其中通过 setter 方法或属性设置依赖项。

SignalR 中的简单依赖关系注入

请考虑使用 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 要求中心类具有无参数构造函数。 但是,可以轻松注册函数以创建中心实例,并使用此函数执行 DI。 通过调用 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 容器为你构造对象,从而“反转”通常的控制流。

在 SignalR 中使用 IoC 容器

聊天应用程序可能太简单,无法从 IoC 容器中受益。 我们来看看 StockTicker 示例。

StockTicker 示例定义了两个main类:

  • StockTickerHub:管理客户端连接的中心类。
  • StockTicker:保存股票价格并定期更新的单一实例。

StockTickerHub 保存对单一实例的 StockTicker 引用,而 StockTicker 保留对 的 IHubConnectionContextStockTickerHub引用。 它使用此接口与 StockTickerHub 实例通信。 (有关详细信息,请参阅使用 ASP.NET SignalR.) 进行服务器广播

我们可以使用 IoC 容器来稍微解开这些依赖项。 首先,让我们简化 StockTickerHubStockTicker 类。 在以下代码中,我注释掉了不需要的部分。

StockTicker中删除无参数构造函数。 相反,我们将始终使用 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 clients)
    {
        if (clients == null)
        {
            throw new ArgumentNullException("clients");
        }

        Clients = clients;
        LoadDefaultStocks();
    }

    //public static StockTicker Instance
    //{
    //    get
    //    {
    //        return _instance.Value;
    //    }
    //}

接下来,可以通过为 StockTicker创建接口来重构代码。 我们将使用此接口将 与 StockTicker 类分离StockTickerHub

Visual Studio 使这种重构变得简单。 打开文件 StockTicker.cs,右键单击 StockTicker 类声明,然后选择“ 重构 ...” 提取接口

显示Visual Studio Code上方的右键单击下拉菜单的屏幕截图,其中突出显示了“重构”和“提取接口”选项。

“提取接口 ”对话框中,单击“ 全选”。 保留其他默认值。 单击 “确定”

“提取接口”对话框的屏幕截图,其中突出显示了“全选”选项并显示“O K”选项。

Visual Studio 将创建名为 IStockTicker的新接口,并将 更改为 StockTickerIStockTicker派生。

打开文件 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创建接口并非严格必要,但我想展示 DI 如何帮助减少应用程序中组件之间的耦合。

添加 Ninject 库

有许多适用于 .NET 的开源 IoC 容器。 在本教程中,我将使用 Ninject。 (其他热门库包括 城堡温莎Spring.NetAutofacUnityStructureMap.)

使用 NuGet 包管理器安装 Ninject 库。 在 Visual Studio 中,从“ 工具 ”菜单中选择“ NuGet 包管理器>包管理器控制台”。 在“Package Manager Console”窗口中,输入以下命令:

Install-Package Ninject -Version 3.0.1.10

替换 SignalR 依赖项解析程序

若要在 SignalR 中使用 Ninject,请创建派生自 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));
    }
}

此类替代 DefaultDependencyResolverGetServiceGetServices 方法。 SignalR 调用这些方法以在运行时创建各种对象,包括中心实例,以及 SignalR 内部使用的各种服务。

  • GetService 方法创建类型的单个实例。 重写此方法以调用 Ninject 内核的 TryGet 方法。 如果该方法返回 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);
    }
}

若要在 Visual Studio 中运行 StockTicker 应用程序,请按 F5。 在浏览器窗口中,导航到 http://localhost:*port*/SignalR.Sample/StockTicker.html

显示在 Internet Explorer 浏览器窗口中的 A S P 点 NET 信号 R 股票代码示例屏幕的屏幕截图。

应用程序的功能与之前完全相同。 (有关说明,请参阅 使用 ASP.NET SignalR 进行服务器广播。) 我们尚未更改行为;只是使代码更易于测试、维护和演变。