Injecting a signalR hub

Topics: Writing modules
Apr 18, 2013 at 9:07 PM
Edited Apr 18, 2013 at 9:30 PM
Have a problem with SingalR and getting an instance of a hub injected into a IBackgroundTask to handle scheduled tasks for the hub.

The (almost) solution I came up with was to make the hub class ChatHub implement an empty interface called IChatHubProxy and then inject that IChatHubProxy into the IBackgroundTask's constructor. Then I cast the interface back to ChatHub to call the clients. Although this appears to work (no errors) the calls never reach the client. Obviously there is something in the wiring that is missing. Any ideas how to do this properly?

I should add that to make this work I add an AutoFac CoreModule to register the interface
        protected override void Load(ContainerBuilder moduleBuilder)
        {
                moduleBuilder.RegisterType<ChatHub>().As<IChatHubProxy >();
        }
Previously I had the Hub injection into IBackgroundTask working but I was doing all the autofacing manually for all the hub's services. After I tried to migrated it all to be more Orchard conventional using IDependency, ISingleton etc.

The following was working for injection purposes, but was less than ideal inside of Orchard for other reasons.
moduleBuilder.Register<ChatHub>(
  c=>
{
  var someRepository = c.Resolve<ISomeinterface>();
 [.....]

return new ChatHub( someRepository, otherService, etc );
).As<IChatHubProxy >();
Developer
Apr 19, 2013 at 10:51 PM
Edited Apr 19, 2013 at 10:52 PM
Dont inject a Hub.

Check out the ForumHandler within this project... https://github.com/Jetski5822/NGM.Wave and trace the code through.

FYI - This module is based on my SignalR module not Piotrs one so I don't know of the differences.
Apr 20, 2013 at 1:10 AM
Thanks Jetski. The use of SignalR with Orchard is touching on a few techs that I am only two-thirds proficient with, so your help is invaluable and greatly appreciated.

I don't believe your module is in the gallery, so I found Piotrs first. I considered switching over to yours but after looking at the code they are so extremely similar that (in theory) should be no difference in application.

Interesting technique in the Wave module. I'll see if I can mimic that. Thanks again.
Apr 20, 2013 at 8:00 PM
Proligence SignalR module should register Hubs just fine without the need for any extra registration.

Can you put the Hub class in the constructor rather than a interface as it should be fine at resolving that over an interface that describes it?
Ive just tried injection a Hub into a MVC Controller and it creates it just fine.

Jetski .... why shouldnt we inject hubs? Their lifetimes should be short, per action, lightweight etc as described in their own documentation. As long as the task is just that then all resources would be free again shortly. If its going to be a long running task then Jetski's implementation is quite valid where you resolve as needed, but that could also be done with the injected variable:
MyConstructor(Work<MyHub> myHubWork, MyHub alsoWorks) { ... } 
myHubWork.Value //being a resolved Hub for when needed. 
I would create it within the constructor of a Service though so that particular actions can be wrapped like Jetski's handler example but just keeping the hub in the constructor rather than resolving it every time its required.

Matt
Apr 20, 2013 at 11:31 PM
Myself I tried both injecting the hub directly and using an interface. Both appear to inject properly but the calls to the client silently fail. No error anywhere.
Apr 21, 2013 at 10:28 AM
Tried the approach suggested by JetSki and even installed his module but it won't work. When the client is called, it hangs the background task. Not sure if its a side affect related to using IBackgroudTask, or some strange issue with other dependencies injected in the hub. Basic approach (trimmed for clarity) is shown below.

End of the day, the hub itself is working as it should. I just need some method to run a timed task that calls clients. Any suggestions?
  public class MarkUsersInactiveTask : IBackgroundTask 
  {
        private readonly IUserActivityHandler_activityHandler;
        private static bool _sweeping = false;

        public MarkUsersInactiveTask(
                  IUserActivityHandler activityHandler
         ){
                 _activityHandler = activityHandler;
          }

          public void Sweep()
          {
              RunSweep();
          }

          private void RunSweep()
          {
                //avoid re-entery if currently running
                 if (_sweeping)
                {
                    return;
                }
               _sweeping = true;

               try
               {
                   CheckUserStatus();
               }
              catch (Exception ex)     { 
                    [...]
               }  finally  {
                  _sweeping = false;
              }
          }

          private void CheckUserStatus( ){
                     ///reduced logic here for making a test
                      _activityHandle.TestCall();
          }
   }
   public interface IUserActivityHandler : IDependency
    {
        void MarkInactive(string roomName, IEnumerable<UserViewModel> users);
        void Leave( string roomName, UserViewModel user);
        //for testing purposes
        void TestCall();
    }

    public class UserActivityHandler : IUserActivityHandler
    {
        private readonly IWorkContextAccessor _workContextAccessor;

        public UserActivityHandler(IWorkContextAccessor workContextAccessor)
        {
            _workContextAccessor = workContextAccessor;
        }

        private IHubContext HubContext
        {
            get { 
                var context = _workContextAccessor.GetContext();
                var connectionManager = context.Resolve<IConnectionManager>();
                var hubContext = connectionManager.GetHubContext<ChatHub>();

                return hubContext;
            }
        }

        public void MarkInactive( string roomName, IEnumerable<UserViewModel> users ){
            HubContext.Clients.Group(roomName).markInactive(users);
        }
  
        public void TestCall(){
                HubContext.Clients.Group("xxx").testCall();
       }

    }
Apr 21, 2013 at 5:11 PM
Edited Apr 21, 2013 at 5:12 PM
JetSki is doing it quite right, my apologies. You cannot grab a hub and use it:
https://github.com/SignalR/SignalR/wiki/Hubs - Broadcasting over a Hub from outside of a Hub

I do have a Background task of a Hub that talks to clients now. I created a little service to create the HubContext
public interface IHubService : IDependency
    {
        IHubContext GetHubContext<THub>()
            where THub : IHub;
    }

    public class HubService : IHubService
    {
        private readonly IConnectionManager connectionManager;

        public HubService(IConnectionManager connectionManager) 
        {
            this.connectionManager = connectionManager;
        }

        public IHubContext GetHubContext<THub>() where THub : IHub
        {
            IHubContext hub = connectionManager.GetHubContext<THub>();

            return hub;
        }
    }
followed with the Background Task:
public class SweepSignalR : IBackgroundTask 
    {
        private static int runEveryMin = 1;
        private static DateTime? started;
        private static DateTime? lastRun; 

        private readonly IClock clock;
        private readonly IHubService hubService;

        public SweepSignalR(IHubService hubService, IClock clock) 
        {
            this.hubService = hubService;
            this.clock = clock;
        }

        public void Sweep()
        {
            if (!started.HasValue) { started = clock.UtcNow; }

            if (!lastRun.HasValue || lastRun.Value.AddMinutes(runEveryMin) <= clock.UtcNow)
                HouseKeeping();
        }

        private void HouseKeeping() 
        {
            if (lastRun == null) { lastRun = clock.UtcNow; }

            var hub = hubService.GetHubContext<SweepHub>();

            hub.Clients.All.informSweep(lastRun, lastRun.Value.AddMinutes(runEveryMin));
        }
    }
I'll chuck a demo up shortly so you can test it. My version of Orchard is quite a bit ahead of 1.6 so it would be good to know if it works.
Apr 21, 2013 at 5:22 PM
Edited Apr 21, 2013 at 6:19 PM
My module and the SignalR module that i use can be found here: https://skydrive.live.com/redir?resid=5638EA7EA8BF9B39!3370&authkey=!ANgxpyItLRokeiY

After adding it; checking that it builds; enabling it; then it has a admin section "Matt's SignalR Tests". This should connect to the hub automaticall. It has a button "pest" which does the same as the background task (try this from another browser). Otherwise it should write a new message every minute when the background task runs.

Thanks, Matt
Apr 21, 2013 at 9:00 PM
Ok thanks Matt, it is greatly appreciated... I was out of ideas. I'll download it and give it a try later tonight and post back on the results.
Apr 23, 2013 at 9:54 PM
Took me forever and a day to figure it out. It just wouldn't work. Finally remembered that pesky setting in VS2012 "Always Start When Debugging" and set it to false and it started working. I was running VS2012 on Windows 7, iis express, clean install of Orchard.

Same 'fix' didn't help in my main orchard install though so now on to solve that. Same symptoms. The background task gets triggered once to never return and the client call fails. I'll post a solution if I find one.
Apr 24, 2013 at 10:19 AM
Edited Apr 24, 2013 at 10:20 AM
Im glad you have managed to get somewhere. I still have some niggles with SignalR like needing to restart casini when Ive made changes (or it will just hang). Ive always thought it was a problem with the lifetime but I haven't managed to figure that out yet.
Which version of Orchard are you running by the way? I'm in a branch quite a bit ahead of 1.6 which I find to be more stable with SignalR. That could also be my imagination though.

If its on the production a restart of the app pool should do the trick. I cant remember if I had troubles publishing to azure when I did it a couple of months ago. I do know where the reset button is so I may have used that too.

Jetski how are you finding SignalR and Orchard handling together?
Apr 24, 2013 at 3:31 PM
I'm running Orchard 1.6. Never tried Cassini with SignalR but I can say that it works without problems using IIS Express. I have no restart issues and it appear perfectly stable.

Only change from the base install is that I've set it to compile against 4.5 instead of 4.0 since I need some 4.5 features. Hard to imagine that 4.5 versus 4.0 could be the source of my problems, but unless I have some strange dll hell going on, I'm not sure what else it would be. Calling var hub = hubService.GetHubContext<SweepHub>(); without actually calling the client is enough that it hangs the backgroundtask. I stripped out all my modules, so this should be a base install again, using only Proligence.SignalR and your module.

I tried your technique with Jetski's implementation and it won't work. Problems resolving the IConnectionManager if I remember correctly. Probably a small reference issue somewhere but didn't have time to work it through.

Anyhow back to pulling out my hair. This is a battle that I need to win.
Developer
Apr 24, 2013 at 3:57 PM
There are issues with Transactions with 1.6 and SignalR. I would upgrade to the tip of 1.x - My module was written against the tip of 1.x and I know Piotrs module took some code out of it for his module so I would imagine its the same issue.

One thing to note, until Orchard upgrades to .net 4.5, you will be using the SignalR SSE and not SignalR Websockets.
Apr 24, 2013 at 4:18 PM
Take back one part of that. Got ahead of myself. When I went back to my orchard build, instead of the test setup, I used a more recent version of SignalR. Just now I installed the one from your skydrive and that one doesn't crash the background task when it gets the hub, but now its back to not actually calling the client. Such strange issues.
Apr 24, 2013 at 4:44 PM
Thanks Jetski I'll try that. Been banging my head against the wall all week with this so hopefully that gets my issue resolved.
Apr 24, 2013 at 8:20 PM
Yup so that was my issue all along... 1.6. Installed the 1.x and it works like it should.

Just a quick question if someone happens to know. Is 1.x db compatible with 1.6?
Developer
Apr 24, 2013 at 10:18 PM
Migrations will always be supported between 1.6 to the tip of 1.x, however, migrations between 1.x and 1.x will not be supported as the features are still in development. You are essentially working on the Dev branch.
Apr 26, 2013 at 7:40 PM
Interestingly enough, I got the dev version setup, got my modules installed and again, suddenly the test case from Matt stopped working again.

After a lot of trouble-guess-shooting, I narrowed it down to the culture picker module

http://gallery.orchardproject.net/List/Modules/Orchard.Module.Orchard.CulturePicker

Disabled it and things started working again.

Went back to version 1.6 of my original build, disabled the Orchard.CulturePicker there and things started working as well. Not sure what the interaction is that breaks the IBackgroundTask, when it gets the hub but thought I'd note it for anyone with a similar issue.

Also noted that ContentMenuItemPart (which is used by the culture picker) was moved out of the Core into Orchard.ContentPicker.Models, so it looks like 1.7 will (potentially) be breaking that module.
May 6, 2013 at 5:39 AM
Edited May 6, 2013 at 5:40 AM
Well I think if have the culture picker issue figured out. Narrowed it down to the class ContentCultureSelector : ICultureSelector {..} and further into the constructure where it has a
public ContentCultureSelector(
         IAliasService aliasService
}
Wrapping the IAliasService in Work<> gets it running again.
 //private readonly IAliasService _aliasService;
 private readonly Work<IAliasService> _aliasServiceWork;

public ContentCultureSelector(
            Work<IAliasService> aliasServiceWork, 
which can be complimented with a getter to keep the original code in tact...

        private IAliasService _aliasService
        {
            get { return _aliasServiceWork.Value; }

        }
I haven't done a ton of testing with this yet but it appears to be the solution so far.