Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

get a list of online users in asp.net mvc

I have a page in my application which always shows updated list of online users. Now, to keep the list-which is stored in application object- updated, i do the below steps

  1. add user to list when login

  2. remove user on log off

  3. Then to handle browser close/navigate away situations, I have a timestamp along with the username in the collection An ajax call every 90 seconds updates the timestamp.

The problem: I need something to clean this list every 120 seconds to remove entries with old timestamps.

How do I do this within my web application? ie Call a function every 2 mins.

PS: I thought of calling a webservice every 2 mins using a scheduler , but the hosting environment do not allow any scheduling.

like image 684
maX Avatar asked Jan 23 '11 14:01

maX


5 Answers

Do the following inside a global filter.

public class TrackLoginsFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Dictionary<string, DateTime> loggedInUsers = SecurityHelper.GetLoggedInUsers();

        if (HttpContext.Current.User.Identity.IsAuthenticated )
        {
            if (loggedInUsers.ContainsKey(HttpContext.Current.User.Identity.Name))
            {
                loggedInUsers[HttpContext.Current.User.Identity.Name] = System.DateTime.Now;
            }
            else
            {
                loggedInUsers.Add(HttpContext.Current.User.Identity.Name, System.DateTime.Now);
            }

        }

        // remove users where time exceeds session timeout
        var keys = loggedInUsers.Where(u => DateTime.Now.Subtract(u.Value).Minutes >
                   HttpContext.Current.Session.Timeout).Select(u => u.Key);
        foreach (var key in keys)
        {
            loggedInUsers.Remove(key);
        }

    }
}

To retrieve the user list

public static class SecurityHelper
{
    public static Dictionary<string, DateTime> GetLoggedInUsers()
    {
        Dictionary<string, DateTime> loggedInUsers = new Dictionary<string, DateTime>();

        if (HttpContext.Current != null)
        {
            loggedInUsers = (Dictionary<string, DateTime>)HttpContext.Current.Application["loggedinusers"];
            if (loggedInUsers == null)
            {
                loggedInUsers = new Dictionary<string, DateTime>();
                HttpContext.Current.Application["loggedinusers"] = loggedInUsers;
            }
        }
        return loggedInUsers;

    }
}

Don't forget to Register you filter in global.asax. It's probably a good idea to have an app setting to switch this off.

GlobalFilters.Filters.Add(new TrackLoginsFilter());

Also remove users at logoff to be more accurate.

SecurityHelper.GetLoggedInUsers().Remove(WebSecurity.CurrentUserName);
like image 166
AntonK Avatar answered Nov 20 '22 20:11

AntonK


In your Account Controller

   public ActionResult Login(LoginModel model, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            if (Membership.ValidateUser(model.UserName, model.Password))
            {
                FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
                if (HttpRuntime.Cache["LoggedInUsers"] != null) //if the list exists, add this user to it
                {
                    //get the list of logged in users from the cache
                    List<string> loggedInUsers = (List<string>)HttpRuntime.Cache["LoggedInUsers"];
                    //add this user to the list
                    loggedInUsers.Add(model.UserName);
                    //add the list back into the cache
                    HttpRuntime.Cache["LoggedInUsers"] = loggedInUsers;
                }
                else //the list does not exist so create it
                {
                    //create a new list
                    List<string> loggedInUsers = new List<string>();
                    //add this user to the list
                    loggedInUsers.Add(model.UserName);
                    //add the list into the cache
                    HttpRuntime.Cache["LoggedInUsers"] = loggedInUsers;
                }
                if (!String.IsNullOrEmpty(returnUrl))
                {
                    return Redirect(returnUrl);
                }
                else
                {

                    return RedirectToAction("Index", "Home");
                }
            }
            else
            {
                ModelState.AddModelError("", "The user name or password provided is incorrect.");
            }
        }

        // If we got this far, something failed, redisplay form
        return View(model);
    }


    public ActionResult LogOff()
    {
        string username = User.Identity.Name; //get the users username who is logged in
        if (HttpRuntime.Cache["LoggedInUsers"] != null)//check if the list has been created
        {
            //the list is not null so we retrieve it from the cache
            List<string> loggedInUsers = (List<string>)HttpRuntime.Cache["LoggedInUsers"];
            if (loggedInUsers.Contains(username))//if the user is in the list
            {
                //then remove them
                loggedInUsers.Remove(username);
            }
            // else do nothing
        }
        //else do nothing
        FormsAuthentication.SignOut();
        return RedirectToAction("Index", "Home");
    }

in your partial view.

@if (HttpRuntime.Cache["LoggedInUsers"] != null)
{
    List<string> LoggedOnUsers = (List<string>)HttpRuntime.Cache["LoggedInUsers"];
    if (LoggedOnUsers.Count > 0)
    {
    <div class="ChatBox">
        <ul>
            @foreach (string user in LoggedOnUsers)
            {
                <li>
                    <div class="r_row">
                       <div class="r_name">@Html.Encode(user)</div>
                    </div>
                </li>
            }
        </ul>
    </div>
    }
}

render this partial view when user log in.

use this script call ever 90 second

<script type="text/javascript">
    $(function () {
        setInterval(loginDisplay, 90000);
    });

    function loginDisplay() {
        $.post("/Account/getLoginUser", null, function (data) {

        });
    }
</script>
like image 5
dev Avatar answered Nov 20 '22 20:11

dev


Here is the white elephant solution.

Instead of maintaining this list in application object, maintain this list in database. Then you can use database jobs to work on this list periodically. Establish SQL notification on this object so that everytime this list is purged you get refreshed data in your application.

like image 2
Pradeep Avatar answered Nov 20 '22 22:11

Pradeep


Use Ajax to send "I am still online" message to the server in every 30 seconds. This is the best way to find who is really online.

like image 1
Cagatay Avatar answered Nov 20 '22 21:11

Cagatay


So here what I did:

  1. Create a table in the database

    CREATE TABLE [dbo].[OnlineUser]
    (
        [ID] [int] IDENTITY(1,1) NOT NULL,
        [Guid] [uniqueidentifier] NOT NULL,
        [Email] [nvarchar](500) NOT NULL,
        [Created] [datetime] NOT NULL,
        CONSTRAINT [PK_OnlineUser] PRIMARY KEY CLUSTERED 
        (
            [ID] ASC
        ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
  2. Override the OnActionExecution method. This method is in a separate controller in my case is called AuthController then every other controller that required authemtication inherits from this controller.

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);
    
        // session variable that is set when the user authenticates in the Login method
        var accessSession = Session[Constants.USER_SESSION];
    
        // load cookie is set when the user authenticates in the Login method
        HttpCookie accessCookie = System.Web.HttpContext.Current.Request.Cookies[Constants.USER_COOKIE];
    
        // create session from cookie
        if (accessSession == null)
        {
            if (accessCookie != null)
            {
                if (!string.IsNullOrEmpty(accessCookie.Value))
                    accessSession = CreateSessionFromCookie(accessCookie);
            }
        }
    
        // if session does not exist send user to login page
        if (accessSession == null)
        {
            filterContext.Result = new RedirectToRouteResult(
                 new RouteValueDictionary
                 {
                     {"controller", "Account"},
                     {"action", "Login"}
                 }
             );
    
             return;
        }
        else 
        {
             TrackLoggedInUser(accessSession.ToString());
        }           
    }
    
    private List<OnlineUser> TrackLoggedInUser(string email)
    {                       
        return GetOnlineUsers.Save(email);            
    }
    
  3. Next I created the following classes in the Data Repository class: GetOnlineUsers

    public static class GetOnlineUsers
    {
        public static List<OnlineUser> GetAll()
        {
            using (var db = new CEntities())
            {
                return db.OnlineUsers.ToList();
            }
        }
    
    
       public static OnlineUser Get(string email)
       {
           using (var db = new CEntities())
           {
               return db.OnlineUsers.Where(x => x.Email == email).FirstOrDefault();
           }
       }
    
       public static List<OnlineUser> Save(string email)
       {
           using (var db = new CEntities())
           {
               var doesUserExist = db.OnlineUsers.Where(x => x.Email.ToLower() == email.ToLower()).FirstOrDefault();
    
               if (doesUserExist != null)
               {
                   doesUserExist.Created = DateTime.Now;
                   db.SaveChanges();
               }
               else
               {
                   OnlineUser newUser = new OnlineUser();
    
                   newUser.Guid = Guid.NewGuid();
                   newUser.Email = email;
                   newUser.Created = DateTime.Now;
    
                   db.OnlineUsers.Add(newUser);
                   db.SaveChanges();
               }
    
               return GetAll();
           }
       }
    
       public static void Delete(OnlineUser onlineUser)
       {
           using (var db = new CEntities())
           {
               var doesUserExist = db.OnlineUsers.Where(x => x.Email.ToLower() == onlineUser.Email.ToLower()).FirstOrDefault();
    
               if (doesUserExist != null)
               {
                   db.OnlineUsers.Remove(doesUserExist);
                   db.SaveChanges();
               }                
           }
       }
    }
    
  4. In the Global.asax

     protected void Application_EndRequest()
     {
         // load all active users
         var loggedInUsers = GetOnlineUsers.GetAll();
    
         // read cookie
         if (Context.Request.Cookies[Constants.USER_SESSION] != null)
         {
             // the cookie has the email
             string email = Context.Request.Cookies[Constants.USER_SESSION].ToString();
    
             // send the user's email to the save method in the repository 
             // notice in the save methos it also updates the time if the user already exist
             loggedInUsers = GetOnlineUsers.Save(email);                
         }
    
         // lets see we want to clear the list for inactive users
         if (loggedInUsers != null)
         {               
             foreach (var user in loggedInUsers)
             {
                 // I am giving the user 10 minutes to interact with the site.
                 // if the user interaction date and time is greater than 10 minutes, removing the user from the list of active user
                 if (user.Created < DateTime.Now.AddMinutes(-10))
                 {
                     GetOnlineUsers.Delete(user);
                 }
             }
         }            
     }
    
  5. In one of the controllers (You can create a new one up to you) that inhering from the AuthController, create the following method:

     public JsonResult GetLastLoggedInUserDate()
     {
         string email = Session[Constants.USER_SESSION].ToString();
    
         var user = GetOnlineUsers.Get(email);
    
         return Json(new {   year = user.Created.Year, 
                             month = user.Created.Month, 
                             day = user.Created.Day, 
                             hours = user.Created.Hour, 
                             minutes = user.Created.Minute, 
                             seconds = user.Created.Second,
                             milliseconds = user.Created.Millisecond
                         }, JsonRequestBehavior.AllowGet);
     }
    
  6. In your _Layout.cshtml file at the very bottom place this Javascript code: This Javascript code will call the GetLastLoggedInUserDate() above to get the last interacted date from the database.

     <script>        
     var lastInteracted, DifferenceInMinutes;
    
     $(window).on('load', function (event) {           
         $.get("get-last-interaction-date", function (data, status) {                                     
             lastInteracted = new Date(data.year.toString() + "/" + data.month.toString() + "/" + data.day.toString() + " " + data.hours.toString() + ":" + data.minutes.toString() + ":" + data.seconds.toString());                                                
         });
     });
    
     $(window).on('mousemove', function (event) { 
         var now = new Date(); 
         DifferenceInMinutes = (now.getTime() - lastInteracted.getTime()) / 60000;  
    
         if (DifferenceInMinutes > 5) {
                 $.get("get-last-interaction-date", function (data, status) {                                     
                     lastInteracted = new Date(data.year.toString() + "/" + data.month.toString() + "/" + data.day.toString() + " " + data.hours.toString() + ":" + data.minutes.toString() + ":" + data.seconds.toString());                                                
                 });
             }
         });
     </script>
    

JavaScript explanation:

On page load I am are setting the last datetime the the user interacted with my website.

Since I cannot track what the user stares at on the screen, the next closest thing to real interaction is mouse movement. So when the user moves the mouse anywhere on the page the following happens:

  1. I compare the last interacted date with the current date.
  2. Then I check if 5 minutes passed since the last updated date occurred.

Since the user happened to love the website and decided to spend more time on it, after the 5 minutes are passed, I send another request to the this method in my controller GetLastLoggedInUserDate() to get the date again. But before we get the date we will execute the OnActionExecuting method which will then update the records Created date and will return the current time. The lastInteracted gets the updated date and we go again.

The idea here is that when the user is not interacting with my website he is not really online for me. Maybe he has 100 tabs open and playing games doing other things but interacting with my website it is possible that they will not even realize they have it open in days or months depends on how often they reboot the PC. In any case I think that 10 minutes is a good threshold to work with but feel free to change it.

Finally AdminController class:

    public ActionResult Index()
    {
        DashboardViewModel model = new DashboardViewModel();

        // loading the list of online users to the dashboard
        model.LoggedInUsers = GetOnlineUsers.GetAll();

        return View("Index", "~/Views/Shared/_adminLayout.cshtml", model);
    }

Index.cshtml (admin dashboard page)

@model ILOJC.Models.Admin.DashboardViewModel

@{
    ViewBag.Menu1 = "Dashboard";
}

/// some html element and styles

<h5 class="">@Model.LoggedInUsers.Count() Online Users</h5>     
<div class="row">
    @foreach (var user in Model.LoggedInUsers.OrderByDescending(x => x.Created))
    {       
       <div class="col-md-12">   
           <h5>@user.Email</h5>                                       
           <p><span>Last Inreaction Time: @user.Created.ToString("MM/dd/yyyy hh:mm:ss tt")</span></p>      
       </div>                                                              
    }
</div>

Since the original table will only store online users I wanted to have a bit of history/log so I create a history table in the database:

CREATE TABLE [dbo].[OnlineUserHistory](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [OnlineUserID] [int] NOT NULL,
    [Guid] [uniqueidentifier] NOT NULL,
    [Email] [nvarchar](500) NOT NULL,
    [Created] [datetime] NOT NULL,
    [Updated] [datetime] NOT NULL,
    [Operation] [char](3) NOT NULL,
 CONSTRAINT [PK_OnlineUserLog] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Lastly, I created a database Trigger on insert and delete

CREATE TRIGGER [dbo].[trg_online_user_history]
ON [dbo].[OnlineUser]
AFTER INSERT, DELETE
AS
BEGIN
    SET NOCOUNT ON;
    INSERT INTO OnlineUserHistory(
        OnlineUserID, 
        [Guid],
        Email,
        Created,        
        Updated, 
        Operation
    )
    SELECT
        i.ID, 
        i.[Guid],
        i.Email,
        i.Created,        
        GETDATE(),
        'INS'
    FROM
        inserted i
    UNION ALL
    SELECT
        d.ID, 
        d.[Guid],
        d.Email,
        d.Created,
        GETDATE(),
        'DEL'
    FROM
       deleted d;
END

Hope this can hep someone. One thing I would improve tho is the way the online users are displaying load in the dashboard. Now, I need to refresh the page to see the updated number. But if you want to see it live, you just add the SignalR library then create a hub and you good to go!

like image 1
Victor_Tlepshev Avatar answered Nov 20 '22 20:11

Victor_Tlepshev