locked
Tracking which users are currently active RRS feed

  • Question

  • User-760709272 posted

    This thread is going to demonstrate how you can track which of your asp.net membership users are currently active. The base project I used was an MVC project with Forms Authentication. None of this code is MVC-only though, the functionality is abstracted into classes so you can easily do the same with a webform project. Similarly if you use a different authentication system you can adapt the code for that as well.

    I've use a repository class to help abstract the code away from the front end, and also because you are more likely to be using this kind of code yourself, but you can just copy the relevant code and put it directly into your controllers or code-behind files if you want. Likewise this also uses Entity Framework but you could use other data access technologies.

    The way the code works in general is that we're going to use a database table to keep track of the last time a user made a request. We'll use the data in this table to work out who is active and who isn't. As well as just recording their last access, we could expand the system to also store the url they last accessed, this will also give you a snapshot of who is currently doing what.

    The second post on this thread will cover some basic unit tests.

    Amend the membership database

    For this demo I'm going to add a new table to track when each person was last active. You could amend the existing User table if you wanted, but it is better to keep your own amendments separate so that you don't have to worry about upgrading your membership database in case the schema changes. You could also use the Profile aspect of Membership to track this information, but it doesn't perform very well and you can't easily use it to run queries against.

    The table I'm creating is called User_Activity and looks like this

    The UserId column equates to the UserId column in aspnet_Users so create a relationship to that table that looks like this

    Implement Entity Framework

    As this demo is only concerned with tracking when users were last active, I'm only going to bind EF to my User_Activity table and the aspnet_Users table. As we created a relationship between the two tables, EF will automatically recognise this relationship. Add the MyMembership.edmx file to the root of the project and import the required tables.

    The properties for the edmx file are as below

    Amend the User_Activity data model

    Entity Framework will create a model class that represents the User_Activity table, and that class is UserTracker.User_Activity. We want to add some custom properties to it, so create a user_Activity partial class inside the Model folder and amend it as follows (note the namespace has been changed to match the one created by EF).

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    
    namespace UserTracker
    {
        // This partial class is "merged" with the class that Entity Framework creates.  That is why it is
        // important that the namespaces match
        public partial class User_Activity
        {
            /// <summary>
            /// Returns the datetime threshold for when a user is considered active
            /// </summary>
            public static DateTime ActiveThreshold
            {
                get
                {
                    // Putting this as a static method on User_Activity lets us define in a single place
                    // what the threshold is defined as.  If we want to change the threshold we only
                    // have to change it in one place
                    return DateTime.Now.AddMinutes(-15);
                }
            }
    
            /// <summary>
            /// Returns if the user is considered to be currently online
            /// </summary>
            public bool IsOnline
            {
                get
                {
                    // User is defined as online if they have been active within
                    // the threshold.
    
                    return this.Last_Activity > ActiveThreshold;
                }
            }
        }
    }

    The reason we add these properties in our own partial class is so that our code doesn't get deleted when EF re-creates its models.

    Create the user activity repository

    We're going to put the code that manages this data into a repository. Create a Repositories folder and create an interface inside it called IUserActivityRepository, and a class called UserActivityRepository that implements this interface. The code in these files are fairly straightforward and listed below;

    using System;
    using System.Collections.Generic;
    
    namespace UserTracker.Repositories
    {
        public interface IUserActivityRepository
        {
            void CreateUser(User_Activity user);
    
            List<User_Activity> GetActiveUsers();
    
            List<User_Activity> GetAllUsers();
    
            User_Activity GetUser(Guid userId);
    
            void UpdateUser(User_Activity user);
        }
    }
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    
    namespace UserTracker.Repositories
    {
        public class UserActivityRepository : IUserActivityRepository
        {
            /// <summary>
            /// Return a User_Activity record based on userId
            /// </summary>
            public User_Activity GetUser(Guid userId)
            {
                using (MyMembership db = new MyMembership())
                {
                    return db.User_Activity.FirstOrDefault(u => u.UserId == userId);
                }
            }
    
            /// <summary>
            /// Create a User_Activity record
            /// </summary>
            public void CreateUser(User_Activity user)
            {
                using (MyMembership db = new MyMembership())
                {
                    db.User_Activity.AddObject(user);
                    db.SaveChanges();
                }
            }
    
            /// <summary>
            /// Update an existing User_Activity record
            /// </summary>
            public void UpdateUser(User_Activity user)
            {
                using (MyMembership db = new MyMembership())
                {
                    db.User_Activity.Attach(user);
                    db.ObjectStateManager.ChangeObjectState(user, System.Data.EntityState.Modified);
                    db.SaveChanges();
                }
            }
    
            /// <summary>
            /// Returns all users, ordered by username
            /// </summary>
            public List<User_Activity> GetAllUsers()
            {
                List<User_Activity> users;
    
                using (MyMembership db = new MyMembership())
                {
                    users = GetUsersQuery(db)
                        .OrderBy(u => u.aspnet_Users.UserName)
                        .ToList();
                }
    
                return users;
            }
    
            /// <summary>
            /// Returns only active users, ordered by time last online
            /// </summary>
            public List<User_Activity> GetActiveUsers()
            {
                List<User_Activity> users;
    
                using (MyMembership db = new MyMembership())
                {
                    users = GetUsersQuery(db)
                        .Where(u => u.Last_Activity > User_Activity.ActiveThreshold)
                        .OrderByDescending(u => u.Last_Activity)
                        .ToList();
                }
    
                return users;
            }
    
            /// <summary>
            /// Returns the base query that gets user data.  Functions that call this
            /// can add there own Where, OrderBy etc clauses as needed
            /// </summary>
            private IQueryable<User_Activity> GetUsersQuery(MyMembership databaseContext)
            {
                return databaseContext.User_Activity
                    .Include("aspnet_Users");
            }
        }
    }



    The above repository gives us all the methods we need to track users' activity records...we can find them, list them, create them and update them.

    Implement the tracking

    We need to fire some code every time the user makes a request so that we can record what user has made the request. There are probably a few places you can do this, I'm going to put the code in the AuthorizeRequest event in the global.asax.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Routing;
    using UserTracker.Repositories;
    
    namespace UserTracker
    {
        public class MvcApplication : System.Web.HttpApplication
        {
            protected void Application_AuthorizeRequest(Object sender, EventArgs e)
            {
                // We only want to run the code for authenticated users
                // As this is an MVC project we also only want to run the code for action requests
    
                // If you are using webforms then don't bother with the RouteData test, it doesn't apply to webforms
    
                if (HttpContext.Current.User.Identity.IsAuthenticated
                    && RouteTable.Routes.GetRouteData(new HttpContextWrapper(HttpContext.Current)) != null)
                {
                    // Create the user activity repository
                    UserActivityRepository userRepository = new UserActivityRepository();
    
                    // Get the userId of the logged in user
                    Guid userId = (Guid)System.Web.Security.Membership.GetUser().ProviderUserKey;
    
                    // Get that user's User_Activity entry
                    User_Activity activity = userRepository.GetUser(userId);
    
                    if (activity == null)
                    {
                        // None was found so this user has no existing activity record so create one
                        activity = new User_Activity
                        {
                            UserId = userId,
                            Last_Activity = DateTime.Now
                        };
    
                        userRepository.CreateUser(activity);
                    }
                    else
                    {
                        // The user has an activity record so update it with the current time
                        // We could also update the current url to give us a snapshot of what users are doing
                        activity.Last_Activity = DateTime.Now;
                        userRepository.UpdateUser(activity);
                    }
    
                }
            }

    Implement the controller

    We'll extend the existing Account controller to implement a Users action that can list all users or just the active ones.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Routing;
    using System.Web.Security;
    using UserTracker.Models;
    using UserTracker.Repositories;
    
    namespace UserTracker.Controllers
    {
        public class AccountController : Controller
        {
            /// <summary>
            /// Private variable to hold the user repository
            /// </summary>
            private IUserActivityRepository userRepository = null;
    
            /// <summary>
            /// Create an account controller with the default UserRepository
            /// </summary>
            public AccountController() : this (new UserActivityRepository())
            {
            }
    
            /// <summary>
            /// Create an account controller using the defined user repository
            /// </summary>
            /// <param name="userRepository"></param>
            public AccountController(IUserActivityRepository userRepository)
            {
                this.userRepository = userRepository;
            }
    
            /// <summary>
            /// Returns the Users view
            /// </summary>
            /// <param name="online">If <value>true</value> then the model will contain only active users, otherwise
            /// the model will contain all users.
            public ActionResult Users(bool? online)
            {
                // You can get all users like this
                // MembershipUserCollection users = Membership.GetAllUsers();
                // However as we need to connect to our custom activity table we're going to access the raw tables
                // via the repository
    
                List<User_Activity> users;
    
                if (online.HasValue && online.Value == true)
                {
                    users = this.userRepository.GetActiveUsers();
                }
                else
                {
                    users = this.userRepository.GetAllUsers();
                }
    
                return View(users);
            }

    Implement the view

    The view is /Views/Account/Users.cshtml

    @model IEnumerable<UserTracker.User_Activity>
    @{
        ViewBag.Title = "Users";
    }
    
    <h2>Users</h2>
    
    <p>
        <a href="@Url.Action("Users", "Account", new { online = false })">View all users</a> |
        <a href="@Url.Action("Users", "Account", new { online = true })">View online users</a>
    </p>
    
    @if (!Model.Any())
    {
        <p>No users</p>
        return;
    }
    
    <table>
        <tr>
            <th>User</th>
            <th>Last activity</th>
            <th>Online</th>
        </tr>
    @foreach (UserTracker.User_Activity user in Model)
    {
        <tr>
            <td>@user.aspnet_Users.UserName</td>
            <td>@user.Last_Activity.ToString("dd MMM yyyy HH:mm:ss")</td>
            <td>@user.IsOnline</td>
        </tr>
    }
    </table>
    

    Navigate to /Account/Users to see the results

    /Accounts/Users/?online=true

    Saturday, May 24, 2014 1:01 PM

All replies

  • User-760709272 posted

    Add some unit tests

    Just for completeness I'm going to add some unit tests. For these tests I am using Moq (https://github.com/Moq/moq4).

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Web.Mvc;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using UserTracker;
    using UserTracker.Controllers;
    using Moq;
    using UserTracker.Repositories;
    
    namespace UserTracker.Tests.Controllers
    {
        [TestClass]
        public class AccountControllerTest
        {
            private List<User_Activity> users = null;
    
            [TestInitialize]
            public void Setup()
            {
                this.users = new List<User_Activity>();
    
                users.Add(new User_Activity
                {
                    aspnet_Users = new aspnet_Users { UserName = "User A" },
                    Last_Activity = User_Activity.ActiveThreshold.AddMinutes(1)
                });
    
            }
    
            [TestMethod]
            public void WhenUsersIsCalledForAllUsersThenAllUsersAreReturned()
            {
                /*
                * Arrange
                */
    
                // Create a mocked IUserRepository
                Mock<IUserActivityRepository> userRepository = new Mock<IUserActivityRepository>();
    
                // It doesn't actually matter that both methods return the same data
                // as we will only be testing the target method has been called and the results
                // sent as the model
    
                userRepository.Setup(ur => ur.GetAllUsers()).Returns(this.users);
                userRepository.Setup(ur => ur.GetActiveUsers()).Returns(this.users);
    
                // Create the controller using our mocked IUserRepository as the repository to use
                AccountController controller = new AccountController(userRepository.Object);
    
                /*
                * Act
                */
    
                ActionResult result = controller.Users(false);
    
                /*
                * Assert
                */
    
                userRepository.Verify(ur => ur.GetAllUsers(), Times.Once());
                userRepository.Verify(ur => ur.GetActiveUsers(), Times.Never());
    
                // The following test makes sure the results of GetAllUsers was returned as a model
                // you probably wouldn't do both these checks in a single test as testing the repository was
                // properly called is a test that is independant of what the action returns.
                // You might not want to even do this check at all
    
                // Make sure a view was returned
                Assert.IsInstanceOfType(result, typeof(ViewResult));
    
                // Get the model data returned with the View
                List<User_Activity> data = (List<User_Activity>) ((ViewResult)result).Model;
    
                Assert.AreSame(this.users, data);
            }
    
            [TestMethod]
            public void WhenUsersIsCalledForActiveUsersThenOnlyActiveUsersAreReturned()
            {
                /*
                * Arrange
                */
    
                Mock<IUserActivityRepository> userRepository = new Mock<IUserActivityRepository>();
    
                userRepository.Setup(ur => ur.GetAllUsers()).Returns(this.users);
                userRepository.Setup(ur => ur.GetActiveUsers()).Returns(this.users);
    
                AccountController controller = new AccountController(userRepository.Object);
    
                /*
                * Act
                */
    
                ActionResult result = controller.Users(true);
    
                /*
                * Assert
                */
    
                userRepository.Verify(ur => ur.GetAllUsers(), Times.Never());
                userRepository.Verify(ur => ur.GetActiveUsers(), Times.Once());
    
                Assert.IsInstanceOfType(result, typeof(ViewResult));
    
                List<User_Activity> data = (List<User_Activity>)((ViewResult)result).Model;
    
                Assert.AreSame(this.users, data);
            }
        }
    }
    

    You might wonder why the tests don't actually verify that only users that have been active within the threshold are returned. When writing unit tests it is important to keep in mind exactly what you are testing and only test the code that you are calling. These tests are controller tests and the only responsibility the controller has is to ensure that the correct repository methods are called, so that is all we are going to unit test. Making sure that the correct users are returned from the database is the responsibility of the repository, not our controller. So tests to ensure that only users active within the threshold are returned would go into the unit tests for the UserActivityRepository. I haven't written these tests because my repository isn't currently unit testable as it replies on the database context supplied by EF. To write unit tests for it I would have to abstract the EF code into its own classes. I'm not going to bother doing that for this demo as we're supposed to be focussing on user tracking, not building a fully testable solution. I just wanted to highlight that your unit tests should only test the code flow of the method you are testing, your tests shouldn't extend to code in other layers and classes.

    Saturday, May 24, 2014 1:03 PM