SignalR 中的依赖项注入
警告
本文档不适用于最新版本的 SignalR。 查看 ASP.NET Core SignalR。
本主题中使用的软件版本
- Visual Studio 2013
- .NET 4.5
- SignalR 版本 2
本主题的早期版本
有关 SignalR 早期版本的信息,请参阅 SignalR 旧版本。
问题和评论
请留下反馈,说明你对本教程的喜爱程度,以及我们可以在页面底部的评论中改进的内容。 如果你有与本教程不直接相关的问题,可以将其发布到 ASP.NET SignalR 论坛 或 StackOverflow.com。
依赖关系注入是一种删除对象之间的硬编码依赖项的方法,可以更轻松地替换对象的依赖项,以便使用 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 注册函数。
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 容器为你构造对象,从而“反转”通常的控制流。
在 SignalR 中使用 IoC 容器
聊天应用程序可能太简单,无法从 IoC 容器中受益。 我们来看看 StockTicker 示例。
StockTicker 示例定义了两个main类:
StockTickerHub
:管理客户端连接的中心类。StockTicker
:保存股票价格并定期更新的单一实例。
StockTickerHub
保存对单一实例的 StockTicker
引用,而 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
创建接口来重构代码。 我们将使用此接口将 与 StockTicker
类分离StockTickerHub
。
Visual Studio 使这种重构变得简单。 打开文件 StockTicker.cs,右键单击 StockTicker
类声明,然后选择“ 重构 ...” 提取接口。
在 “提取接口 ”对话框中,单击“ 全选”。 保留其他默认值。 单击 “确定” 。
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
创建接口并非严格必要,但我想展示 DI 如何帮助减少应用程序中组件之间的耦合。
添加 Ninject 库
有许多适用于 .NET 的开源 IoC 容器。 在本教程中,我将使用 Ninject。 (其他热门库包括 城堡温莎、 Spring.Net、 Autofac、 Unity 和 StructureMap.)
使用 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));
}
}
此类替代 DefaultDependencyResolver 的 GetService 和 GetServices 方法。 SignalR 调用这些方法以在运行时创建各种对象,包括中心实例,以及 SignalR 内部使用的各种服务。
- GetService 方法创建类型的单个实例。 重写此方法以调用 Ninject 内核的 TryGet 方法。 如果该方法返回 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 类中的 Startup.ConfigureSignalR 方法:
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);
}
}
若要在 Visual Studio 中运行 StockTicker 应用程序,请按 F5。 在浏览器窗口中,导航到 http://localhost:*port*/SignalR.Sample/StockTicker.html
。
应用程序的功能与之前完全相同。 (有关说明,请参阅 使用 ASP.NET SignalR 进行服务器广播) 我们尚未更改行为;只是使代码更易于测试、维护和改进。