4

I have a Signalr Hub called NotificationHub that handles sending new notification to connected clients. The NotificationHub class uses a NotificationManager class to retrieve notifications data. Now, I want to be able to use a session to store the last time a new notification has been accessed but when using HttpContext.Current.Session["lastRun"] in NotificationManager I get a NullReferenceException. To clarify more, here is some of the codes of both classes:

NotificationHub

[HubName("notification")]
    public class NotificationHub : Hub
    {
        private NotificationManager _manager;
        private ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public NotificationManager Manager
        {
            get { return _manager; }
            set { _manager = value; }
        }

        public NotificationHub()
        {
            _manager = NotificationManager.GetInstance(PushLatestNotifications);
        }

        public void PushLatestNotifications(ActivityStream stream)
        {
            logger.Info($"Adding {stream.TotalItems} notifications ");
            Clients.Caller.addLatestNotifications(stream);
        }
//.....
}

NotificationManager

 public class NotificationManager
        {
            private static NotificationManager _manager;
            private DateTime _lastRun;
            private DbUpdateNotifier _updateNotifier;
            private readonly INotificationService _notificationService;
            private readonly Action<ActivityStream> _dispatcher;
            private long _userId;
            private IUnitOfWork unitOfWork;
            public NotificationService NotificationService => (NotificationService)_notificationService;
            public DbUpdateNotifier UpdateNotifier
            {
                get { return _updateNotifier; }
                set { _updateNotifier = value; }
            }

            public static NotificationManager GetInstance(Action<ActivityStream> dispatcher)
            {
                return _manager ?? new NotificationManager(dispatcher);
            }



            private NotificationManager(Action<ActivityStream> dispatcher)
            {
                _userId = HttpContext.Current.User.Identity.CurrentUserId();
                _updateNotifier = new DbUpdateNotifier(_userId);
                _updateNotifier.NewNotification += NewNotificationHandler;
                unitOfWork = new UnitOfWork();
                _notificationService = new NotificationService(_userId, unitOfWork);
                _dispatcher = dispatcher;

            }



         private void NewNotificationHandler(object sender, SqlNotificationEventArgs evt)
                {

                  //Want to store lastRun variable in a session here
                    var notificationList = _notificationService.GetLatestNotifications();
                    _dispatcher(BuilActivityStream(notificationList));
                }
           //....
}

I want to able to store the value of lastRun to a session to that I can retrieve the next time a new notification arrives. How can I achieve that?

Edit:

To clarify things up, what I want to store in session is the last time the server pushed new notification(s) to the client. I can use this value to only get notifications that happened after the current value of lastRun and then update lastRun to DateTime.Now. For example: Let's say a user has three new(unread) notifications and then two new notifications arrive. In this case the server has to know the time the last new notifications have pushed to the client so that it will only send those two new notifications.

xabush
  • 849
  • 1
  • 13
  • 29
  • `private static DateTime lastRun` – freedomn-m Apr 29 '16 at 15:54
  • Correct me if I'm wrong but changing *lastRun* to a static field will make it to be used by separate users. But here, I want to store a session for each user. – xabush Apr 29 '16 at 16:08
  • Is your session configured to use a database, or in process session? Because using In Process Session to do this would be bad. Do you have a database for these users? And a database layer using something like EF6, NHibernate, PetaPoco, etc? If so add a field to a poco for the users and store it there. – Ryan Mann Apr 29 '16 at 16:21
  • I'm using SQL Server to store the sessions. – xabush Apr 29 '16 at 16:27
  • You can use HttpContext.Current.Session anywhere as long as long as you touch it after BeginRequest and before EndRequest in IIS request life cycle. – Ryan Mann Apr 29 '16 at 16:30
  • 1
    "*I want to be able to store the last time a new notification has been accessed*" "*so that I can retrieve the next time a new notification arrives*" - these imply you want to know when a "new notification" occurs globally, not per user. Can you clarify in the question text? – freedomn-m Apr 30 '16 at 15:23
  • @freedomn-m I have edited my question to include more information. Let me know if there is anything I can add. – xabush Apr 30 '16 at 15:39
  • It's useful to know what you're trying to do because this is a classic [XY Problem](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) - ie you've got a problem, come up with a possible solution and asked how to fix the issue you get with your (incorrect) solution to the actual problem. – freedomn-m May 01 '16 at 22:52
  • 1
    SignalR is designed to be "always connected" (in a simplified manner) - so when you get "new notifications", you send them on to the clients, immediately - there's no need to for a "last sent/received". When would you even trigger this? (when would you compare the current time vs the stored time? In what context etc?) Sounds like you're thinking in controller/actions rather than signalr. – freedomn-m May 01 '16 at 22:53
  • @freedomn-m, So what do you suggest I should do in this case? – xabush May 03 '16 at 09:38
  • 1
    Just use SignalR. SignalR does what you want, as you've described it, out of the box. ofc I could be missing something. If you're not sure, start again with SignalR from the ground up. – freedomn-m May 03 '16 at 09:42

4 Answers4

3

If you are okay with another data source, I would recommend abstracting it through DI as @Babak alludes to.

Here is what I did for the problem - this should do the trick.

I am partial to Autofac, but any IoC component will work.

  1. Define two Interfaces (NotificationUpdateService and NotificationUpdateDataProvider).

The NotificationUpdateService is what you will interact with. The NotificationUpdateDataProvider abstracts out the backing store - you can change it to whatever. For this example I used the cache object.

public interface INotificationUpdateDataProvider
{
    string UserId { get;  }
    DateTime LastUpdate { get; set; }
}

public interface INotificationUpdateService
{
    DateTime GetLastUpdate();

    void SetLastUpdate(DateTime timesptamp);
}
  1. Implement the the interfaces. The data provider is a simple class that uses the HttpContext. From there we get the userId - to make this implementation specific to the user.

For the Cache Item- I defined a Dictionary object - with the UserId as a key and a DateTime as the value.

public class NotificationUpdateDataProvider : INotificationUpdateDataProvider
{
    private readonly Dictionary<string, DateTime> _lastUpdateCollection;
    private readonly string _userId;
    private Cache _cache;

    public NotificationUpdateDataProvider()
    {
        _cache = HttpRuntime.Cache;
        //Stack Overflow - get the User from the HubCallerContext object
        //http://stackoverflow.com/questions/12130590/signalr-getting-username
        _userId = Context.User.Identity.GetUserId();
        _lastUpdateCollection =(Dictionary<string,DateTime>) _cache["LastUpdateCollection"];

        //If null - create it and stuff it in cache
        if (_lastUpdateCollection == null)
        {
            _lastUpdateCollection = new Dictionary<string, DateTime>();
            _cache["LastUpdateCollection"] = _lastUpdateCollection;
        }
    }

    public DateTime LastUpdate
    {
        get { return _lastUpdateCollection[_userId]; }

        set
        {
            //add to existing or insert new
            if (_lastUpdateCollection.ContainsKey(_userId))
            {
                _lastUpdateCollection[_userId] = value;
            }
            else
            {
                _lastUpdateCollection.Add(_userId, value);
            }    

        }
    }

    public string UserId => _userId;
}



public class NotificationUpdateService : INotificationUpdateService
{
    private readonly INotificationUpdateDataProvider _provider;

    public NotificationUpdateService(INotificationUpdateDataProvider provider)
    {
        _provider = provider;
    }

    public DateTime GetLastUpdate()
    {
        return _provider.LastUpdate;
    }

    public void SetLastUpdate(DateTime timestamp)
    {
        _provider.LastUpdate = timestamp;
    }
}
  1. I added in another static class for the Autofac registration:

    public static void RegisterComponents()
    {
        var builder = new ContainerBuilder();
    
        //First register the NotificationDataProvider
        builder.RegisterType<NotificationUpdateDataProvider>()
            .As<INotificationUpdateDataProvider>();
    
        //Register the update service
        builder.RegisterType<NotificationUpdateService>()
            .As<INotificationUpdateService>();
    
        var container = builder.Build();
    
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    
    }
    
  2. Update the Global.asax

        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        //I am partial Autofac - but unity, Ninject, etc - the concept is the same
        AutofacConfig.RegisterComponents();
    
  3. You will need to modify the constructor to public if you want Autofac to resolve the service.

    public class NotificationManager
    {
            private static NotificationManager _manager;
            private DateTime _lastRun;
            private DbUpdateNotifier _updateNotifier;
            private readonly INotificationService _notificationService;
            private readonly Action<ActivityStream> _dispatcher;
            private long _userId;
            private IUnitOfWork unitOfWork;
            public NotificationService NotificationService => (NotificationService)_notificationService;
            private readonly INotificationUpdateService _updateService;                                         
            public DbUpdateNotifier UpdateNotifier
            {
                get { return _updateNotifier; }
                set { _updateNotifier = value; }
            }
    
            public static NotificationManager GetInstance(Action<ActivityStream> dispatcher)
            {
                return _manager ?? new NotificationManager(dispatcher);
            }
    
    
            //You'll need to make the constructor accessible for autofac to resolve your dependency
            public NotificationManager(Action<ActivityStream> dispatcher,  INotificationUpdateService updateService)
            {
                _userId = HttpContext.Current.User.Identity.CurrentUserId();
                _updateNotifier = new DbUpdateNotifier(_userId);
                _updateNotifier.NewNotification += NewNotificationHandler;
                unitOfWork = new UnitOfWork();
                _notificationService = new NotificationService(_userId, unitOfWork);
                _dispatcher = dispatcher;
                _updateService = updateService;
            }
    
    
    
            private void NewNotificationHandler(object sender, SqlNotificationEventArgs evt)
            {
    
                //Want to store lastRun variable in a session here
    
                //just put the datetime in through the service
                _updateService.SetLastUpdate(DateTime.Now);
    
                var notificationList = _notificationService.GetLatestNotifications();
                _dispatcher(BuilActivityStream(notificationList));
            }
    
    
    }
    
  4. If you do not want to modify your constructor - then just do this:

            //This is not the preferred way  - but it does the job
            public NotificationManager(Action<ActivityStream> dispatcher)
            {
                _userId = HttpContext.Current.User.Identity.CurrentUserId();
                _updateNotifier = new DbUpdateNotifier(_userId);
                _updateNotifier.NewNotification += NewNotificationHandler;
                unitOfWork = new UnitOfWork();
                _notificationService = new NotificationService(_userId, unitOfWork);
                _dispatcher = dispatcher;
                _updateService = DependencyResolver.Current.GetService<INotificationUpdateService>(); //versus having autofac resolve in the constructor
            }
    
  5. Finally - use it:

       private void NewNotificationHandler(object sender, SqlNotificationEventArgs evt)
            {
    
                //Want to store lastRun variable in a session here
    
                //just put the datetime in through the service
                _updateService.SetLastUpdate(DateTime.Now);
    
                var notificationList = _notificationService.GetLatestNotifications();
                _dispatcher(BuilActivityStream(notificationList));
            }
    

This doesn't use session - but it does solve what you are trying to do. It also gives you an element of flexibility in that you can change out your backing data providers.

JDBennett
  • 1,323
  • 17
  • 45
  • Your approach is good. However, HttpContext.Current is null sometimes and raises NullReferenceException. – xabush Jun 23 '16 at 14:24
  • I'm a bit confused then. How is it you have _userId = HttpContext.Current.User.Identity.CurrentUserId(); working? Thinking about this a bit more. The only way the HttpContext could be null is if a thread is spun off. If a request from the client (in this case the poll from connected clients to the hub) - you get a context in the request. You don't get session, but you DO get a context. From there you can get the identity and the cache. – JDBennett Jun 23 '16 at 18:08
  • Doing some research on this. All you really need is the Identity of the user. Found [this](http://stackoverflow.com/questions/12130590/signalr-getting-username) on how you can get that using signal R. Having this in place - I will modify my solution so it works. Also found out WHY HttpContext.Current is null (sometimes). [This](http://www.asp.net/signalr/overview/guide-to-the-api/hubs-api-guide-server#nocallerstate) probably explains it. – JDBennett Jun 23 '16 at 18:18
  • I have updated the solution. I changed the HttpContext.Current - to Context. Since you are using SignalR - I am assuming you have app.MapSignalR() in your startup somewhere. This will give you access to the HubCallerContext (which incidentally will yield the context). – JDBennett Jun 23 '16 at 18:35
0

As @Ryios mentioned, you can access HttpContext.Current.Session. The main issue, though, is that HttpContext.Current is null when you are not in an HTTP context; for example when you are running your unit tests. What you are looking for for Dependency Injection.

HttpContext.Current.Session is an instance of System.Web.SessionState.HttpSessionState so you could update your NotificationManager constructor to accept an instance of HttpSessionState and the controller calling into it would pass HttpContext.Current.Session as a parameter.

Using your example, the call to NotificationManager.GetInstance would change to

    public NotificationHub()
    {
        _manager = NotificationManager.GetInstance(PushLatestNotifications, HttpContext.Current.Session);
    }
Babak Naffas
  • 12,395
  • 3
  • 34
  • 49
  • I don't **HttpContext.Current.Session** is available in Signalr 2.x. See these questions. [Why is HTTPContext.Current.Session null using SignalR 2.x libraries in a ASP .Net MVC application?](http://stackoverflow.com/questions/22023544/why-is-httpcontext-current-session-null-using-signalr-2-x-libraries-in-a-asp-ne) , [SignalR doesn't use Session on server](http://stackoverflow.com/questions/7854663/signalr-doesnt-use-session-on-server) – xabush Apr 30 '16 at 08:05
0

Per this answer:

You shouldn't use Session with SignalR (see SignalR doesn't use Session on server). You identify logical connections by their connection id which you can map to user names.

The underlying problem is that access to SessionState is serialized in ASP.NET to ensure state consistency, so each request to the hub would block other requests. In the past, limited read-only access (I assume (but can't confirm since the gist is gone) by setting EnableSessionstate to read-only, which prevents the locking problem I described) was possible, but support for this was dropped. Also see various other places where the SignalR team made similar statements. Lastly: there's a statement in the official documentation about HTTPContext.Current.Session.

I would just mark this as an exact duplicate of the question, but since you have a bounty this question cannot be closed.

Community
  • 1
  • 1
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
0

You can follow the below solution, it works for me fine -

Notification HUB Code Behind -

public class NotificationsHub : Hub
{
    public void NotifyAllClients(string s_Not, DateTime d_LastRun)
    {
       IHubContext context = GlobalHost.ConnectionManager.GetHubContext<NotificationsHub>();
       context.Clients.All.displayNotification(s_Not, d_LastRun);
    }
}

You can supply the variables to Notification HUB using below way (for example, you can change as per your need) -

NotificationsHub nHub = new NotificationsHub();
nHub.NotifyAllClients("Test Notification", Now.Date);

Now if you want to save the last run time in session variable you can do it using Javascript -

<script type="text/javascript">            
        $(function () {
            var notify = $.connection.notificationsHub;

            notify.client.displayNotification = function (s_Not, d_LastRun) {
                "<%=System.Web.HttpContext.Current.Session("lastRun")="' + d_LastRun + '"%>";                    
            };

            $.connection.hub.start();

        });

    </script> 

I hope this would help.

meghlashomoy
  • 123
  • 1
  • 9