I apologise for the somewhat confusing nature of the title; I was unsure how to properly word it. If there's a better way of classifying what I'm trying to do, I will try to reword the title (and this post) for greater clarity.
I have a SignalR hub, BaseHub, that inherits from Hub<T>. BaseHub is then inherited by TestHub. This hub is mapped via `routes.MapHub
The BaseHub class is stored in a library, along with some controllers. The TestHub class is stored in the ASP.NET Core API project itself, which contains more controllers.
When injecting the HubContext into one of my controllers in the API project I specify IHubContext<TestHub, ITestClient> hubContext as the constructor parameter, which works as expected.
When injecting the HubContext into one of my controllers in the library project I specify IHubContext<BaseHub, ITestClient> hubContext as the constructor parameter, which does _not_ work as expected. There are no exceptions thrown, and a HubContext instance is supplied, but SignalR notifications are not sent using this instance.
When looking at the non-public members of this HubContext instance in the watch window, I can see that HubContext.Clients._lifetimeManager._connections.Count is 0 even though I know there are definitely connections to the hub (for the one that works, this figure is above zero when I inspect the HubContext instance using the watch window).
My best guess is it's expecting there to be a different hub instance available of type BaseHub.
What can I do to ensure that injections of IHubContext<TestHub, ITestClient> and IHubContext<BaseHub, ITestClient> reference the same hub instance?
For example, with my DbContext classes, I am able to do services.AddScoped<BaseDbContext>(f => f.GetRequiredService<DerivedDbContext>());, however that doesn't seem possible here. For starters, the MapHub() part happens in Configure() rather than ConfigureServices(). Even if I ignore that, and attempt to add the service regardless, it looks like IHubContext doesn't support covariance (I think that's the correct term?), so the following throws an InvalidCastException:
services.AddScoped<IHubContext<BaseHub<ITestClient>>>(f => (IHubContext<BaseHub<ITestClient>>)f.GetRequiredService<IHubContext<TestHub>>());
Is what I am attempting possible? Or is my only alternative to either move those shared controllers out of the library project, or to create derived controllers that inject the correct HubContext?
Hey, @Metritutus when you use MapHub
With this you can inject in D.I a singleton like that: MapHub<TestHub>
you can use MapHub<BaseHub> and it will get your hub from the dependency container.
I don't know why when you requested an IHubContext
services.TryAddSingleton(typeof(IHubContext<>), typeof(HubContext<>));
services.TryAddSingleton(typeof(IHubContext<,>), typeof(HubContext<,>));
By default this wont work, but you might be able to set it up to work this way by doing something like: services.AddSingleton<IHubContext<BaseHub>, IHubContext<TestHub>>();
Hey, @Metritutus when you use MapHub it will add a connection middleware, it uses ActivatorUtilities.GetServiceOrCreateInstance() for create or get your hub one time (when you add);
With this you can inject in D.I a singleton like that:
and instead use MapHub<TestHub>
you can useMapHub<BaseHub>and it will get your hub from the dependency container.I don't know why when you requested an IHubContext
it doesn't throw an exception, probably because IHubContext is injected in D.I in this way: services.TryAddSingleton(typeof(IHubContext<>), typeof(HubContext<>)); services.TryAddSingleton(typeof(IHubContext<,>), typeof(HubContext<,>));
I had a little trouble following what you were saying, but based on your post here, I did the following:
In ConfigureServices(), after the call to services.AddSignalR(), I added the following:
```c#
services.AddSingleton
And I changed my `MapHub()` call to be `MapHub<BaseHub>()`.
It works for the controllers that inject `IHubContext<BaseHub, ITestClient>` at that point, but not the ones that inject `IHubContext<TestHub, ITestClient>`. I believe this is solely due to the change in the `MapHub()` call, rather than the `AddSingleton()` calls; I'm not sure they made any difference.
I also tried (with no luck):
```c#
services.AddSingleton<TestHub>();
services.AddSingleton<BaseHub>(f => f.GetRequiredService<TestHub>());
Am I missing anything?
By default this wont work, but you might be able to set it up to work this way by doing something like:
services.AddSingleton<IHubContext<BaseHub>, IHubContext<TestHub>>();
Unfortunately this doesn't compile because IHubContext doesn't support covariance, I believe? "There is no implicit conversion". Am I missing something?
In addition, because it's strongly typed, it would need to be services.AddSingleton<IHubContext<BaseHub, ITestClient>, IHubContext<TestHub, ITestClient>>();
Now, I understand, you wanna use 2 hubs that do the same thing? Why your controllers use an IHubContext<TestHub, ITestClient> instead of <IHubContext<BaseHub, ITestClient>?
I think signalR supports one hub per path.
I believe I've just understood what it is you were saying!
You are suggesting I always inject IHubContext<BaseHub, ITestClient>, and do MapHub<BaseHub>() because the actual Hub type doesn't matter, as the main thing that matters here for utilising the hub context is ITestClient! For the hub itself, it will be the correct type behind the scenes because it'll have been added using AddSingleton<BaseHub, TestHub>()!
I shall test this and report back.
UPDATE: So this _does_ work for the dependency injection of IHubContext for both controllers. _However_, the use of MapHub<BaseHub>() means that external clients (the SignalR Javascript client in this case) trying to send SignalR notifications which use methods on TestHub cannot do so because those methods do not exist in the BaseHub type. This is a shame, as I'm not sure there's a way to work around this for this particular approach, unfortunately.
I realise I probably should have tagged you in my original response @BrennanConroy, as you have marked this as Resolved and will probably therefore not have reason to check back without the tag. The code proposed in your response sadly does not compile: https://github.com/dotnet/aspnetcore/issues/23534#issuecomment-652667643
I think has a logic error here, when you add something in dependency with the overload <TService, TImplementation> you do it for an interface/abstract/base class when you do it you wanna get a TService type without know which is your implementation, in your case you can use your BaseHub as an abstract class and put the abstract methods that exist in TestHub in this BaseHub.
After all, when you use this overload the dependency container won't resolve the TestHub for you because you wanna TService, or in this case, BaseHub
I think you can see more here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1
MapHub<BaseHub> basically will do IServiceProvider.GetService<BaseHub> if this injected.
I think has a logic error here, when you add something in dependency with the overload
<TService, TImplementation>you do it for an interface/abstract/base class when you do it you wanna get aTServicetype without know which is your implementation, in your case you can use yourBaseHubas an abstract class and put the abstract methods that exist inTestHubin thisBaseHub.After all, when you use this overload the dependency container won't resolve the
TestHubfor you because you wannaTService, or in this case,BaseHubI think you can see more here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1
MapHub<BaseHub>basically will doIServiceProvider.GetService<BaseHub>if this injected.
I'm afraid that's not what I'm getting at. My reply highlights the fact that this solution works for the controllers in the API and the class library (injecting IHubContext<BaseHub, ITestClient>), but has the unfortunate side-effect of rendering the public methods of TestHub inaccessible to external clients like the Javascript SignalR client.
I believe this is because MapHub<BaseHub> is done with the BaseHub type and not the TestHub type. My guess is reflection is used somewhere under the hood to generate the relevant endpoints using the supplied type.
Oh sure, I think in this case is better for you just use BaseHub and TestHub with differents paths or your TestHub can call the BaseHub methods.
This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.
See our Issue Management Policies for more information.
This issue still isn't resolved for me. @BrennanConroy, unfortunately the approach you suggested does not compile, which I believe is because IHubContext interface does not support covariance.
This scenario isn't really supported. I provided pseudo code, which I guess unfortunately doesn't work.
One possible approach you can take is to make your own abstraction and the implementation of that would have a reference to the TestHub class.
```c#
// referenced by your controller that doesn't know about TestHub
public class ICustomHubContext
{
// methods you want to call on the IHubContext
}
// implementation provided by your app
public class CustomHubContext : ICustomHubContext
{
private readonly IHubContext
public CustomHubContext(IHubContext<TestHub> hubContext)
{
_context = hubContext;
}
// methods that pass-through to _context
}
```
Ah, I had feared that there would be no further responses after this issue was closed!
Whilst it looks like your proposal will work, (being that the implementation of CustomHubContext is defined only in the API project), you lose the ability to easily interact with IHubClients without building some sort of parallel abstraction, or something that's far more limited by hard-coding which clients you want to utilise.
I guess as you say, this scenario is sadly not really supported. Adding covariance support for IHubContext, etc, would help here, but I guess there's probably reasons why that isn't already the case. Thanks for your response though!
Most helpful comment
Hey, @Metritutus when you use MapHub it will add a connection middleware, it uses ActivatorUtilities.GetServiceOrCreateInstance() for create or get your hub one time (when you add);
With this you can inject in D.I a singleton like that: and instead use
MapHub<TestHub>you can use
MapHub<BaseHub>and it will get your hub from the dependency container.I don't know why when you requested an IHubContext it doesn't throw an exception, probably because IHubContext is injected in D.I in this way: