seesharper / LightInject

An ultra lightweight IoC container
http://www.lightinject.net
MIT License
615 stars 122 forks source link

SignalR Integration #215

Open joelbourbon opened 8 years ago

joelbourbon commented 8 years ago

Recently I used LighInject to replace SignalR DependencyResolver. Unfortunately, I had some issues and those could have been avoided with better documentation.

Basic Information Using the following Nuget Packages:

First thing The documentation exhibits a method that does not actually exists: serviceContainer.RegisterHubs();

Second thing If you use the documented way of integrating LightInject in SignalR, the serviceContainer.EnableSignalR() method only replaces the DependencyResolver within the HubConfiguration and not within the GlobalHost. Therefore, if you want to use a hub outside of its context using GlobalHost.ConnectionManager.GetHubContext<myHub>(), the returned hub will be different and you won't be able to communicate with the clients.

The following StackOverflow question explains better the issues I encountered.

Finally

212 is a pull request to fix the missing dependency within the LightInject.SignalR Nuget package. Also, when using the "non-source" nugets, for the LightInject.SignalR Nuget package to work with its dependency (LightInject.Interception), you need to add an explicit call to the library for it to be outputed in your binaries. Otherwise the library won't work. Ex.:

public class AgentManagementStartup
{
    public void ConfigurationOwin(IAppBuilder app, IAgentManagerDataStore dataStore)
    {
        var serializer = new JsonSerializer
        {
            PreserveReferencesHandling = PreserveReferencesHandling.Objects,
            TypeNameHandling           = TypeNameHandling.Auto,
            TypeNameAssemblyFormat     = FormatterAssemblyStyle.Simple
        };

        // DO NOT REMOVE ==> Explicit usage of LightInject.Interception for the DLL to
        // be included in the output
        // ReSharper disable once UnusedVariable
        var includLightInjectInterceptionByExplicitUsage = new MethodSelector();

        var container = new ServiceContainer();
        container.RegisterInstance(dataStore);
        container.RegisterInstance(serializer);
        container.Register<EventHub>();
        container.Register<ManagementHub>();
        var config = container.EnableSignalR();

        GlobalHost.DependencyResolver = config.Resolver;
        GlobalHost.HubPipeline.AddModule(new LoggingPipelineModule());

        app.MapSignalR("", config);
    }
}

If there is any way I could help with those issues, please indicate how :+1:

CC: @cstlaurent

puzsol commented 8 years ago

For those who come across this issue (like I did), I found the RegisterHubs code somewhere online (I forget if I modified it, but here is what I use):

public static class ServiceContainerExtensions
    {
        /// <summary>
        /// Registers all <see cref="Controller"/> implementations found in the given <paramref name="assemblies"/>.
        /// </summary>
        /// <param name="serviceRegistry">The target <see cref="IServiceRegistry"/>.</param>
        /// <param name="assemblies">A list of assemblies from which to register <see cref="Controller"/> implementations.</param>
        public static void RegisterHubs(this IServiceRegistry serviceRegistry, params Assembly[] assemblies)
        {
            foreach (var assembly in assemblies)
            {
                var hubTypes = assembly.GetTypes().Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(Hub)));
                foreach (var hubType in hubTypes)
                {
                    serviceRegistry.Register(hubType, new PerScopeLifetime());
                }
            }
        }

        /// <summary>
        /// Registers all <see cref="Controller"/> implementations found in this assembly.
        /// </summary>
        /// <param name="serviceRegistry">The target <see cref="IServiceRegistry"/>.</param>
        public static void RegisterHubs(this IServiceRegistry serviceRegistry)
        {
            RegisterHubs(serviceRegistry, Assembly.GetCallingAssembly());
        }
    }

Even with that, I hit a worse issue than missing the Intercept dependency, even once I included the package, the code just didn't work... I was getting exceptions that I couldn't track down, if I tried to make a call to the service it shut down the application pool.... Not fun.

Before I tried the lightinject.signalr module I was using the container.EnablePerWebRequestScope(); method instead, which worked when I was using a long polling connection, but as soon as I enabled the websocket transport, the LightInject scope was never set properly... it was a mess.

So In the end I took a similar approach to setting the scope on a per-request basis for Hubs...

Added these classes:

public class LightInjectScopedConnection : HubDispatcher
    {
        private IServiceContainer _container;

        public LightInjectScopedConnection(IServiceContainer container, HubConfiguration config)
            :base(config)
        {
            this._container = container;
        }

        protected override Task OnReceived(IRequest request, System.String connectionId, System.String data)
        {
            _container.BeginScope();
            base.OnReceived(request, connectionId, data).Wait();
            _container.EndCurrentScope();
            return Task.CompletedTask;
        }

        public override Task ProcessRequest(HostContext context)
        {
            _container.BeginScope();
            base.ProcessRequest(context).Wait();
            _container.EndCurrentScope();
            return Task.CompletedTask;
        }
    }

internal class LightInjectSignalRDependencyResolver : DefaultDependencyResolver
    {
        private readonly IServiceFactory serviceFactory;
        public LightInjectSignalRDependencyResolver(IServiceContainer serviceFactory)
        {
            this.serviceFactory = serviceFactory;
        }

        public override object GetService(Type serviceType)
        {
            return serviceFactory.TryGetInstance(serviceType) ?? base.GetService(serviceType);
        }

        public override IEnumerable<object> GetServices(Type serviceType)
        {
            return serviceFactory.GetAllInstances(serviceType).Concat(base.GetServices(serviceType));
        }
    }

Then used it as:

public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var container = new ServiceContainer();
            container.ScopeManagerProvider = new PerThreadScopeManagerProvider();
            container.RegisterHubs();
            container.Register<LightInjectScopedConnection>();

            var config = new HubConfiguration();

            container.RegisterInstance<IServiceContainer>(container);
            container.RegisterInstance<HubConfiguration>(config);
            config.Resolver = new LightInjectSignalRDependencyResolver(container);
            GlobalHost.DependencyResolver = config.Resolver;

            app.MapSignalR<LightInjectScopedConnection>("/signalr", config);
        }
    }

The use of LightInjectScopedConnection for the connection, allows me to intercept the ProcessRequest for regular web requests, connecting, pinging, etc; and OnReceived for the websocket calls.

At the moment it is working, so I'm hoping this is the end of it (at least until the Lightinject.SignalR release is ??fixed??).... Even managed to get it to pass an event message from Rebus down to the client (which was the whole point for me doing this in the first place).

Let me know if you find another, better solution, or have any enhancements for me to use :smile:

stefankip commented 3 years ago

I had to change the LightInjectScopedConnection to this:

public class LightInjectScopedConnection : HubDispatcher
{
    private readonly IServiceContainer _container;

    public LightInjectScopedConnection(IServiceContainer container, HubConfiguration config)
        : base(config)
    {
        _container = container;
    }

    protected override Task OnReceived(IRequest request, string connectionId, string data)
    {
        using var scope = _container.BeginScope();
        base.OnReceived(request, connectionId, data);

        return Task.CompletedTask;
    }

    public override Task ProcessRequest(HostContext context)
    {
        using var scope = _container.BeginScope();
        base.ProcessRequest(context).Wait();

        return Task.CompletedTask;
    }
}

So the scope is changed and I removed the Wait() call in the OnReceived method. With the Wait() call every second message to the hub kept hanging for some reason.