According to this post, it should be possible to inject per-web-request dependencies into SignalR hubs (although with some limitations like problem with OnDisconnected() method). In my case it is ASP Web API (not MVC) and it does not work for some reason.
Here are relevant parts:
container.RegisterWebApiControllers(httpConfiguration);
container.RegisterWebApiRequest<DbContext, MyDbContext>();
container.RegisterWebApiRequest<ISampleRepository, SampleRepository>(); //DbContext injected to SampleRepository
//Enable injections to SignalR Hubs
var activator = new SimpleInjectorHubActivator(container);
GlobalHost.DependencyResolver.Register(typeof(IHubActivator), () => activator);
This class makes possible to inject into hubs:
public class SimpleInjectorHubActivator : IHubActivator
{
private readonly Container _container;
public SimpleInjectorHubActivator(Container container)
{
_container = container;
}
public IHub Create(HubDescriptor descriptor)
{
return (IHub)_container.GetInstance(descriptor.HubType);
}
}
And Hub itself:
[HubName("sample")]
public class SampleHub : Hub
{
public ActiveBetsHub(ISampleRepository repository)
{
}
//Irrelevant methods here. OnDisconnected() NOT implemented!
}
With this setup I get exception:
No registration for type SampleHub could be found and
an implicit registration could not be made.
The ISampleRepository is registered as 'Web API Request'
lifestyle, but the instance is requested outside the context of a Web API Request.
Which is expected as I understand. However I get exactly same exception when I change Lifestyle of repository to Transient:
var transientHybrid = Lifestyle.CreateHybrid(() => HttpContext.Current != null, new WebApiRequestLifestyle(), Lifestyle.Transient);
container.Register<ISampleRepository, SampleRepository>(transientHybrid);
I suspect the problem could lie in HttpContext.Current != null
check that is not working for Web API the same way as for MVC.
SignalR 2.2
Simple Injector 2.8.3
What do I miss?
UPDATE:
This is stack trace on how SignalR creates Hubs:
at SimpleInjector.InstanceProducer.GetInstance()
at SimpleInjector.Container.GetInstance(Type serviceType)
at MyWebAbi.WebApiApplication.SimpleInjectorHubActivator.Create(HubDescriptor descriptor) in Global.asax.cs:line 108
at Microsoft.AspNet.SignalR.Hubs.DefaultHubManager.ResolveHub(String hubName)
at Microsoft.AspNet.SignalR.Hubs.HubDispatcher.CreateHub(IRequest request, HubDescriptor descriptor, String connectionId, StateChangeTracker tracker, Boolean throwIfFailedToCreate)
So the proper solution would be to use ExecutionContextScope
for a Hubs but this scope needs to be explicitly closed which makes things more complicated...
Your definition of your hybrid lifestyle is incorrect. The WebApiRequestLifestyle
does not depend in any way on the HttpContext
so checking whether HttpContext.Current != null
will not work. You will have to check if there is an active Web API request lifestyle scope (or execution context scope, which is basically the same) by calling container.GetCurrentExecutionContextScope()
:
var transientHybrid = Lifestyle.CreateHybrid(
() => container.GetCurrentExecutionContextScope() != null,
new WebApiRequestLifestyle(),
Lifestyle.Transient);
Do note however that you should be very careful composing a hybrid lifestyle of a scoped lifestyle and transient, because this will easily yield in wrong results. This is actually the default behavior of some DI libraries, but this is a design flaw IMO. I assume you very consciously registered your MyDbContext
with the scoped lifestyle, because you need to make sure that the same instance is used throughout the request. Using the Transient
lifestyle means that you might get multiple MyDbContext
during the request. This might not be a problem, because in your hubs you might currently only have one reference to your MyDbContext
, but your code might break once your object graph changes and a second reference to MyDbContext
is added.
So instead, I would advice not using this combination of lifestyles. Instead, just use either the WebApiRequestLifestyle
or the ExecutionContextScopeLifestyle
(they are the same) and make sure that such a execution context scope is started before your hub is resolved.
And by the way, don't forget to register your hubs explicitly in Simple Injector. This allows Simple Injector to analyze the complete object graph for you including your hub classes.
Recently I faced the same problem and found the following working quite well, hope this will help someone:
public class SignalRDependencyResolver : DefaultDependencyResolver
{
public SignalRDependencyResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override object GetService(Type serviceType)
{
return _serviceProvider.GetService(serviceType) ?? base.GetService(serviceType);
}
public override IEnumerable<object> GetServices(Type serviceType)
{
var @this = (IEnumerable<object>) _serviceProvider.GetService(typeof (IEnumerable<>).MakeGenericType(serviceType));
var @base = base.GetServices(serviceType);
return @this == null ? @base : @base == null ? @this : @this.Concat(@base);
}
private readonly IServiceProvider _serviceProvider;
}
public class SignalRHubDispatcher : HubDispatcher
{
public SignalRHubDispatcher(Container container, HubConfiguration configuration) : base(configuration)
{
_container = container;
}
protected override Task OnConnected(IRequest request, string connectionId)
{
return Invoke(() => base.OnConnected(request, connectionId));
}
protected override Task OnReceived(IRequest request, string connectionId, string data)
{
return Invoke(() => base.OnReceived(request, connectionId, data));
}
protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
{
return Invoke(() => base.OnDisconnected(request, connectionId, stopCalled));
}
protected override Task OnReconnected(IRequest request, string connectionId)
{
return Invoke(() => base.OnReconnected(request, connectionId));
}
private async Task Invoke(Func<Task> method)
{
using (_container.BeginExecutionContextScope())
await method();
}
private readonly Container _container;
}
public class Startup
{
public void Configuration(IAppBuilder app)
{
var container = new Container();
container.Options.DefaultScopedLifestyle = new ExecutionContextScopeLifestyle();
container.Register<DbContext, MyDbContext>(Lifestyle.Scoped);
container.Register<ISampleRepository, SampleRepository>(Lifestyle.Scoped);
// if you want to use the same container in WebApi don't forget to add
app.Use(async (context, next) => {
using (container.BeginExecutionContextScope())
await next();
});
// ... configure web api
var config = new HubConfiguration
{
Resolver = new SignalRDependencyResolver(container)
}
// ... configure the rest of SignalR
// pass SignalRHubDispatcher
app.MapSignalR<SignalRHubDispatcher>("/signalr", config);
}
}