想把我的 2 美分和其他答案一起扔在这里,这有助于在 SignalR 中找到自己的依赖注入方式,无论是使用 SimpleInjector 还是其他 IoC。
如果您决定使用 Steven 的答案,请确保在编写根之前注册您的集线器路由。 SignalRRouteExtensions.MapHubs 扩展方法(又名routes.MapHubs())将在映射枢纽路线时在GlobalHost.DependencyResolver 上调用Register(Type, Func<object>),因此如果在映射路线之前将DefaultDependencyResolver 替换为Steven 的SimpleInjectorResolver,您将碰到他的NotSupportedException。
这是我的最爱。为什么?
- 代码少于
SimpleInjectorDependencyResolver。
- 无需替换
DefaultDependencyResolver(又名GlobalHost.DependencyResolver),这意味着更少的代码。
- 您可以在映射集线器路由之前或之后组成根,因为您没有替换
DefaultDependencyResolver,它会“正常工作”。
就像 Nathanael 说的那样,这只是在你关心 Hub 类的依赖关系的情况下,这可能是大多数人的情况。如果您想将其他依赖项注入 SignalR 中,您可能想要使用 Steven 的答案。
Hub 中每个 Web 请求的依赖关系问题
SignalR 有一个有趣的地方...当客户端与集线器断开连接时(例如通过关闭其浏览器窗口),它将创建Hub 类的新实例以调用OnDisconnected()。发生这种情况时,HttpContext.Current 为空。因此,如果这个 Hub 有任何按网络请求注册的依赖项,可能会出错。
在忍者中
我使用 Ninject 和 ninject signalr dependency resolver on nuget 尝试了 SignalR 依赖注入。使用此配置,绑定.InRequestScope() 的依赖项将在断开连接事件期间注入Hub 时临时创建。由于HttpContext.Current 为空,我想Ninject 只是决定忽略它并创建瞬态实例而不告诉你。也许有一个配置设置告诉 ninject 对此发出警告,但这不是默认设置。
在 SimpleInjector 中
另一方面,当 Hub 依赖于向 WebRequestLifestlyle 注册的实例时,SimpleInjector 将引发异常:
NameOfYourHub 类型的注册委托引发了异常。这
NameOfYourPerRequestDependency 类型的注册委托引发了异常。这
YourProject.Namespace.NameOfYourPerRequestDependency 注册为
'PerWebRequest',但实例是在上下文之外请求的
一个 HttpContext(HttpContext.Current 为空)。确保实例使用
这种生活方式在应用程序初始化期间没有解决
阶段和在后台线程上运行时。用于解析实例
在后台线程上,尝试将此实例注册为 'Per Lifetime
范围':https://simpleinjector.readthedocs.io/en/latest/lifetimes.html#scoped.
...请注意,此异常只会在 HttpContext.Current == null 时冒泡,据我所知,仅当 SignalR 请求 Hub 实例以调用 OnDisconnected() 时才会发生。
Hub 中每个 Web 请求依赖项的解决方案
请注意,这些都不是真正理想的,这完全取决于您的应用程序要求。
在忍者中
如果您需要非瞬态依赖项,请不要覆盖 OnDisconnected() 或使用类依赖项进行任何自定义。如果这样做,图中的每个依赖项都将是一个单独的(瞬态)实例。
在 SimpleInjector 中
您需要一个介于 WebRequestLifestlye 和 Lifestyle.Transient、Lifestyle.Singleton 或 LifetimeScopeLifestyle 之间的 hybrid lifestyle。当HttpContext.Current 不为空时,依赖项将仅与您通常期望的 Web 请求一样长。但是,当HttpContext.Current 为空时,依赖项将作为单例或在生命周期范围内临时注入。
var lifestyle = Lifestyle.CreateHybrid(
lifestyleSelector: () => HttpContext.Current != null,
trueLifestyle: new WebRequestLifestyle(),
falseLifestyle: Lifestyle.Transient // this is what ninject does
//falseLifestyle: Lifestyle.Singleton
//falseLifestyle: new LifetimeScopeLifestyle()
);
更多关于LifetimeScopeLifestyle
就我而言,我有一个 EntityFramework DbContext 依赖项。这些可能很棘手,因为它们可能会在临时注册或作为单例注册时暴露问题。临时注册时,您可能会在尝试使用附加到 2 个或更多 DbContext 实例的实体时遇到异常。当注册为单例时,您最终会遇到更一般的异常(永远不要将DbContext 注册为单例)。在我的情况下,我需要 DbContext 在特定的生命周期内生存,在该生命周期内,同一实例可以在许多嵌套操作中重复使用,这意味着我需要 LifetimeScopeLifestyle。
现在,如果您将上面的混合代码与 falseLifestyle: new LifetimeScopeLifestyle() 行一起使用,当您的自定义 IHubActivator.Create 方法执行时,您将得到另一个异常:
NameOfYourHub 类型的注册委托引发了异常。这
NameOfYourLifetimeScopeDependency 注册为“LifetimeScope”,
但是该实例是在生命周期范围的上下文之外请求的。
确保先调用 container.BeginLifetimeScope()。
您设置生命周期范围依赖的方式如下:
using (simpleInjectorContainer.BeginLifetimeScope())
{
// resolve solve dependencies here
}
在生命周期范围内注册的任何依赖项都必须在此 using 块内解析。此外,如果这些依赖项中的任何一个实现了IDisposable,它们将在using 块的末尾被处理掉。不要试图做这样的事情:
public IHub Create(HubDescriptor descriptor)
{
if (HttpContext.Current == null)
_container.BeginLifetimeScope();
return _container.GetInstance(descriptor.HubType) as IHub;
}
我问过 Steven(如果你不知道的话,他也是 SimpleInjector 的作者),他说:
嗯.. 如果你不处置 LifetimeScope,你会大吃一惊
麻烦,所以确保他们得到处理。如果您不处置
范围,它们将在 ASP.NET 中永远存在。这是因为
范围可以嵌套并引用其父范围。所以一个线程
使最内部的范围(及其缓存)保持活动状态,并且此范围保持
激活它的父作用域(带有它的缓存)等等。 ASP.NET 池
线程,当它从
池,所以这意味着所有范围都保持活动状态,下次你
从池中获取一个线程并开始一个新的生命周期范围,您将
只需创建一个新的嵌套范围,这将继续堆叠。
迟早你会得到一个 OutOfMemoryException。
您不能使用IHubActivator 来确定依赖项的范围,因为它不会像它创建的Hub 实例那样存在。因此,即使您将 BeginLifetimeScope() 方法包装在 using 块中,您的依赖项也会在 Hub 实例创建后立即释放。您真正需要的是另一层间接。
在 Steven 的帮助下,我最终得到了一个命令装饰器(和一个查询装饰器)。 Hub 不能依赖于 per-web-request 实例本身,而是必须依赖于另一个接口,其实现取决于 per-request 实例。注入到Hub 构造函数中的实现用一个包装器装饰(通过simpleinjector),该包装器开始并处理生命周期范围。
public class CommandLifetimeScopeDecorator<TCommand> : ICommandHandler<TCommand>
{
private readonly Func<ICommandHandler<TCommand>> _handlerFactory;
private readonly Container _container;
public CommandLifetimeScopeDecorator(
Func<ICommandHandler<TCommand>> handlerFactory, Container container)
{
_handlerFactory = handlerFactory;
_container = container;
}
[DebuggerStepThrough]
public void Handle(TCommand command)
{
using (_container.BeginLifetimeScope())
{
var handler = _handlerFactory(); // resolve scoped dependencies
handler.Handle(command);
}
}
}
... 装饰的 ICommandHandler<T> 实例依赖于每个网络请求的实例。有关所用模式的更多信息,请阅读this 和this。
示例注册
container.RegisterManyForOpenGeneric(typeof(ICommandHandler<>), assemblies);
container.RegisterSingleDecorator(
typeof(ICommandHandler<>),
typeof(CommandLifetimeScopeDecorator<>)
);