This repository has been archived by the owner. It is now read-only.

IHubContext<THub> "Singleton access" (Dependency Injection) #2347

Closed
animet opened this Issue May 22, 2018 · 11 comments

Comments

Projects
None yet
5 participants
@animet
Copy link

animet commented May 22, 2018

Hi there,
im currently working with ASP.NET Core SignalR 1.0.0-rc1-final (.Net Core 2.1.300-rc1).

In my application I want to be able to access my hub/clients anytime from the backend (singleton).
As far as I have studied the source code, access to e.g. the IHubContext<THub> is only possible in a scoped manner. Or did I miss something?

I already tried to register my own service as a singleton and inject the IHubContext. However, there were no connections in there. I think, because I was out of scope he created a new object. When I inject in e.g. my HomeController's constructor I get the correct, scoped, IHubContext with the expected number of connections. (https://github.com/aspnet/SignalR/issues/1699)

In SignalR for ASP.NET I can get access to the IHubContext e.g. via GlobalHost.ConnectionManager.GetHubContext<THub>(); (see https://docs.microsoft.com/en-us/aspnet/signalr/overview/getting-started/tutorial-high-frequency-realtime-with-signalr#add-the-server-loop )

My current, and dirty approach, is to make a new class which inherits from the DefaultHubDispatcher. This class stores a static variable of the injected IHubContext. Then I add my custom HubDispatcher as a singleton to the .Net DI.

Another idea I had was to write my own HubLifetimeManager (however, calling the public virtual ValueTask WriteAsync(SerializedHubMessage message, CancellationToken cancellationToken = default) method in application code is not intended.

Furthermore, I thought of adding/removing my typed clients in an static accessible concurrent dictionary in my custom hub implementation in the OnConnectAsync and OnDisconnectAsync methods of the Hub.

@animet animet changed the title SignalR IHubContext<THub> "Singleton access" (Dependency Injection) IHubContext<THub> "Singleton access" (Dependency Injection) May 22, 2018

@davidfowl

This comment has been minimized.

Copy link
Member

davidfowl commented May 22, 2018

IHubContext is a singleton and already works as you would expect without writing custom code. Just inject it into your class and you can call methods.

@anurse anurse added this to the Discussions milestone May 22, 2018

@animet

This comment has been minimized.

Copy link
Author

animet commented May 23, 2018

Hi David,

I figured out was the problem was: I used a different IoC Container. In the ConfigureServices method in my Startup class I build a new container with services.BuildServiceProvider(). I used this service provider to retrieve my singletons and inject objects.

Injecting the IHubContext in the ctor of my controllers works, as you mentioned. So it seems as long as I use the ASP.NET Core Dependency Injection (i.e. injecting in controllers, etc) I receive the correct object. I now inject the IHubContext in the Configure method of my Startup class. This article provides more information: https://weblogs.asp.net/ricardoperes/asp-net-core-inversion-of-control-and-dependency-injection

@dasjestyr

This comment has been minimized.

Copy link

dasjestyr commented Jun 2, 2018

@davidfowl I'm actually using this method but the instance that is injected appears to have no connections. I've verified that my hub is communicating with my frontend perfectly fine from the hub directly, but when I try to send a message to the same methods from a class that injects IHubContext, the messages do not go out. When I debug and dig through the private fields, the list of connections has a count of 0. I've tried sending messages directly to Clients.Client(connectionId) and also Clients.All with no success.

@davidfowl

This comment has been minimized.

Copy link
Member

davidfowl commented Jun 2, 2018

Put a repro of the bug somewhere. It’s likely you’re creating another DI container or something strange like that. To prove it, make a new project and try to create a minimal repro of the bug.

@dasjestyr

This comment has been minimized.

Copy link

dasjestyr commented Jun 2, 2018

I'm certainly creating another DI container (Autofac)...

I'm using NServiceBus and they don't yet have an IServiceCollection adapter , so I'm populating the Autofac container from the IServicesCollection. I'll comb through that again and see if it's not transferring over some registrations groan

@dasjestyr

This comment has been minimized.

Copy link

dasjestyr commented Jun 3, 2018

I wasn't able to find the registration in the Autofac builder... interesting. I removed that from the pipeline and tried a direct registration with NServiceBus and still no luck. I tested the DI with a controller to rule out a problem with the SignalR installer, but it worked fine. I'll take this up with Particular. Thanks.

@davidfowl

This comment has been minimized.

Copy link
Member

davidfowl commented Jun 3, 2018

Sounds like you probably have 2 container universes. That won’t work. You need to capture the instance from the web application.

@dasjestyr

This comment has been minimized.

Copy link

dasjestyr commented Jun 3, 2018

@davidfowl

Actually you know what, I tried a more direct approach and it's not working. So if I request the service in something like the Hub ctor, it comes in fine. I can see it has 1 connection. But if I request the service from IServiceCollection.GetRequiredService<> then I get an instance with no connections:

public static class NsbInstaller
{
    public static void AddNsb(this IServiceCollection services)
    {
        var licensePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "NSB_License.xml");

        var config = new EndpointConfiguration("SignalRTest");
        config.LicensePath(licensePath);
        config.UseTransport<LearningTransport>();
        
        config.RegisterComponents(
            configComp =>
            {
                configComp.ConfigureComponent(() =>
                    GetHubContext(services), 
                    DependencyLifecycle.InstancePerCall);
            });

        var session = Endpoint.Start(config).Result;
        services.AddSingleton<IMessageSession>(session);
    }

    private static IHubContext<MessageHub, IClientSystemUpdate> GetHubContext(IServiceCollection services)
    {
        var provider = services.BuildServiceProvider();

        // BUG: if you observe client connections here, it says 0 instead of 1 every time
        var hub = provider.GetRequiredService<IHubContext<MessageHub, IClientSystemUpdate>>();
        return hub;
    }
}

I verified that every time my handler spins up by placing a break point in GetHubContext. it calls that factory method every time because I have it set to request it on every request just in case there's some weird state capture happening. So in other words, I'm watching it call the DI system directly and it's still giving me a context with no connections. I'm not sure why this would be...

... maybe RegisterComponents is capturing serviceCollection and not letting it update? That seems odd to me, because if nothing else, I would think that the registrations aren't changing, only the fields in them, by reference, right? Unless something silly is happening in the pipeline where registrations are being unregistered and then re-registered as connections are being added and subtracted?

@dasjestyr

This comment has been minimized.

Copy link

dasjestyr commented Jun 3, 2018

Repro: https://github.com/dasjestyr/SignalRTest1

NSB Installer is where IServiceCollection is called per request; located at DependencyInjection/NsbInstaller.cs

If you run the demo, you can see that the messages go out when the Hub is contacted directly, but the DI'd service Handlers/Stage1Update.cs pushes a message out to a context with no connections so the output never makes it to the frontend. I also created a controller to test if that DI pipeline is working, and it is. For whatever reason, it seems that factory method that calls serviceCollection in the NsbInstaller always gets a context with no connections.

There is also a demo of how it was done in an older version that didn't have this problem since it was getting the context from what looked like a service locator: https://docs.particular.net/samples/near-realtime-clients/

@dasjestyr

This comment has been minimized.

Copy link

dasjestyr commented Jun 3, 2018

I think I figured it out. I needed to completely replace IServiceProvider. In my NSB installer, I fill the Autofac container and then use that to load AutofacServiceProvider and then return that from the installer, I also ensure that is the last thing in the chain of services that are configured. Back in the Startup.cs I change

public void ConfigureServices(IServiceCollection services)
to
public IServiceProvider ConfigureServices(IServiceCollection services)

and then return the new provider from the NSB installer. I forgot you could even do that...

e.g.

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    // other registrations happen here...
    
    // this is the last one since it's what creates the autofac provider
    // from IServiceCollection
    return services.AddNsb();
}

Clearly there's some voodoo going on in that implementation that keeps those things in sync. Seems that everything is working now. Looking forward to Particular creating an official adapter for their DI...

@aspnet-hello

This comment has been minimized.

Copy link

aspnet-hello commented Sep 24, 2018

We periodically close 'discussion' issues that have not been updated in a long period of time.

We apologize if this causes any inconvenience. We ask that if you are still encountering an issue, please log a new issue with updated information and we will investigate.

@aspnet-hello aspnet-hello removed this from the Discussions milestone Sep 24, 2018

@aspnet aspnet locked and limited conversation to collaborators Sep 24, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.