C# asp.net MVC: When to update LastActivityDate? - c#

I'm using ASP.NET MVC and creating a public website. I need to keep track of users that are online. I see that the standard way in asp.net of doing this is to keep track of LastActivityDate. My question is when should I update this?
If I update it every time the users clicks somewhere, I will feel a performance draw back. However if I do not do that, people that only surf around will be listed as offline.
What is the best way to do this in asp.net MVC?

Just put an ajax javascript call at the bottom of your master page to track this.
Don't worry about performance at this time. If it's implemented and you see it being a problem then come back to finding a better solution. Something so simple shouldn't be a performance problem.
Just think about it like Google analytics. It sits at the bottom of millions of pages with little to no impact on the user experiences of those sites.

Just ran into the same problem, here's my answer for MVC users:
The idea is to trigger Membership.GetUser("..", true) for every page load. This will automatically update the LastActivityDate.
I put this in my global.asax under "RegisterGlobalFilters":
filters.Add(new MembershipTriggerLastActivityDate());
I created a new class that looks like this:
class MembershipTriggerLastActivityDate : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
MembershipUser user = Membership.GetUser(filterContext.HttpContext.User.Identity.Name, true);
}
base.OnActionExecuting(filterContext);
}
}

I started using the SimpleMembershipProvider. It's so simple that there's no more LastActivityDate tracking. So I had to roll my own.
I just added a LastActivityDate column in the Users table and was good to go...
Following #Jab's tip and using the _Layout.cshtml page (master page) in an ASP.NET MVC app, I did this with the help of jQuery:
$(document).ready((function () {
var isUserAuthenticated = '#User.Identity.IsAuthenticated';
if (isUserAuthenticated) {
$.ajax({
type: "POST",
url: "#Url.Action(MVC.Account.ActionNames.UpdateLastActivityDate, MVC.Account.Name)",
data: { userName: '#User.Identity.Name' },
cache: false
});
}
});
Here's the action method:
public virtual ActionResult UpdateLastActivityDate(string userName)
{
User user = Database.Users.Single(u => u.UserName == userName);
user.LastActivityDate = DateTime.Now;
Database.Entry(user).State = EntityState.Modified;
Database.SaveChanges();
return new EmptyResult();
}
Only 133ms (YMMV) :-)

Why not implement the update to LastActivityDate as an asynchronous call? That way you can fire the update and continue processing.

As #Jab says, just implement it and if you see it as a performance issue in the future - deal with it then.
This is how I've done it in my application:
protected void Application_EndRequest()
{
if ((Response.ContentType == "text/html") && (Request.IsAuthenticated))
{
var webUser = Context.User as WebUser;
if (webUser != null)
{
//Update their last activity
webUser.LastActivity = DateTime.UtcNow;
//Update their page hit counter
webUser.ActivityCounter += 1;
//Save them
var webUserRepo = Kernel.Get<IWebUserRepository>(); //Ninject
webUserRepo.Update(webUser);
}
}
}
I haven't had any problems with performance.
HTHs,
Charles

I put it into a special queue that allows only one of a given key to be in the queue (and use the userId as the key in this case). Then I have a low priority thread that works its way through this queue doing database updates. Thus no slow down for the user, and one user doing 100 updates in one second doesn't cause any harm. If it ever becomes an issue I'll make those updates into batch updates against the database but for now this approach works just fine.
If the application crashed I'd lose a few seconds of last activity data but that's just fine. Of course I also update the in memory User object every time so that it's reflected in the UI even if it hasn't made its way to the database yet. Typically it's there before they've received the completed page anyway.

If you are using InProc SessionState, use the SessionStateModule.End event. This happens when the session state is evicted from the underlying cache storage. Typically this happens after 20 minutes of inactivity, you can set the time in web.config.

Good question, have thought about that too and how accurate these mechanisms can be considering performance, a couple of ideas:
1) Track the the last login date
2) Use the LastLoginDate + the expected session length to set some kind of LastOnlineDate that can be used to check if the user is online.

I don't think there is big penalty in performance if you fetch the current logged-in user on every request and update the LastActivityDate field every time (if you have care and invoke the GetUser method for the logged-in user once per http-request). In this way you can also make sure you always have the user's data fresh, like email, name, etc in case he/she updates that data.

I tried Charlino's code in the Global.asax like this
protected void Application_BeginRequest(object sender, EventArgs e)
{
if ((Response.ContentType == "text/html") && (Request.IsAuthenticated))
{
}
}
However I was getting the Request.IsAuthenticated false all the time.
So I moved the code to a method in my Site.Master pagelike this
public void RegisterActivity()
{
if ((Response.ContentType == "text/html") && (Request.IsAuthenticated))
{
string userName = Page.User.Identity.Name;
UserManager userManager = new UserManager();
AppUser appUser = userManager.FindByName(userName);
appUser.LastActivityDate = DateTime.UtcNow;
userManager.Update(appUser);
}
}
I call the method from the Master page Page_Load event, and there it worked.
I'm using asp.net Identity not Membership but I added an AppUser class inheriting from the IdentityUser class and in the AppUser class I added LastActivityDate property.
This is in a WebForms Applicaction not MVC.

Related

Global redirect back to front page if claim is empty or does not exist (NOT identity verification)

Just to be clear: I am NOT talking about claims-based identity validation.
I am building an app in which I make fine use of Identity 2.2 to provide validation. It is sufficient for my needs.
My problem is that once a user logs in, only the first page is widely accessible without storing additional information in the user’s “session”. In particular, when the user clicks on a major internal item (for sake of convenience, let’s call this a “customer module”, the Guid for that customer is stored in a claim held by the user. That way, the user can move from page to page and still have that same customer’s data brought up on every page regardless of what chunk of data the page was meant to display. This claim is only refreshed with something different when they return to the main page and click on another customer.
For security’s sake I would like to ensure that if a claim gets accidentally dropped or set to empty, the user gets shunted back to the main page regardless of where they are in the system, and preferably without having to put code in each and every page action of every controller.
Suggestions? Or am I completely wrong by making use of claims? Because it’s still early enough in the project to make a u-turn if the advantages of a different method are compelling enough.
EDIT:
Just to let people know my solution: Because only one group of people will be accessing this application (the users that interact with companies, this app is to record the interactions and “company information”), I decided to use a base controller. The users would be able to log on and view lists of companies without coming across any page that derived from BaseController, but once they chose a Company to work with, they needed to have Claims set to be able to maintain page-by-page contact with this company’s information. This information would be reset only when they chose a different company, but since there was always a chance that a claim could be disabled, I needed something to automagically redirect them back to the list of companies to re-set the claims. A BaseController that was employed by only those pages where information specific to one company would be displayed was the perfect solution.
A base controller is simple. Just create a controller called BaseController and you’re off to the races. Change any controller that needs to work with this base controller such that they are public class YourOtherController : BaseController.
I initially tried to make an Initialize method to handle everything, but ran into a rather big problem: I was unable to successfully both access and write to my Claims. As in, I was able to either read my claims but not make use of my ClaimWriter extension, or I was able to make use of my ClaimWriter extension but be unable to read claims in the first place. Since Initialize is wayyyy too low in the stack to actually do both these things, I abandoned it and went for an OnActionExecuted method, which ended up being successful. My code ended up being this:
public class BaseController : Controller {
private ApplicationDbContext db = new ApplicationDbContext();
protected override void OnActionExecuted(ActionExecutedContext filterContext) {
base.OnActionExecuted(filterContext);
var principal = ClaimsPrincipal.Current.Identities.First();
var company = User.GetClaimValue("CWD-Company");
var prospect = User.GetClaimValue("CWD-Prospect");
if(string.IsNullOrEmpty(company)) {
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.Redirect("/");
filterContext.HttpContext.Response.End();
}
if(!string.IsNullOrEmpty(company) && string.IsNullOrEmpty(prospect)) {
var id = new Guid(company);
var prospecting = db.Prospecting
.Where(x => x.CompanyId.Equals(id))
.Select(x => x.ProspectingId)
.ToList().SingleOrDefault();
if(prospecting.Equals(Guid.Empty)) { // null prospecting
User.AddUpdateClaim("CWD-Prospecting", "");
} else { // fill prospecting
User.AddUpdateClaim("CWD-Prospecting", Convert.ToString(prospecting));
}
}
}
}
I am probably going to change the if(prospecting.Equals(Guid.Empty) part of the Prospecting section to automagically create the first entry in the db (with all null values except for the ProspectingId and the CompanyId, of course), but this is what works for now.
That's a fine use of claims you describe, no need a u-turn. What you need is a MVC filter, authorisation filter. Something like this:
public class MyAuthorisationFilter : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
var principal = HttpContext.Current.User as ClaimsPrincipal;
if(!principal.Claims.Any(c => c.Type == "My Claim Name"))
{
// user has no claim - do redirection
// you need to create 'AuthenticateAgain' route to your table of routes
// or you can do other means of redirection
filterContext.Result = new RedirectToRouteResult("AuthenticateAgain", new RouteValueDictionary());
}
}
}
Then you can add it globally in your filters configuration, but you'll have to exclude your authorisation page from this filter. Or apply on per controller basis - whenever this needs to happen.
This is very basic form of filter - a lot of checks are stripped out, but it gives a general direction how to proceed.
Update
This is a good article about Authorise attribute.
Here use of AllowAnonymous attribute is explained
The way you use it - depends on your scenario. In most cases when you only expose a login page to the world - it is sufficient to add this attribute as a global filter (see second link, part about RegisterGlobalFilters) and then sprinkle [AllowAnonymous] on top of controllers/actions which should be exposed without authentication.
Another approach is to have a base controller that has your attribute applied. And then all your controllers inherit from this base controller. This is more sufficient when global filter does not cut it: cases when you expose different pages to different users - think companies and customers. Your controllers for companies will inherit CompaniesBaseController that has [CompaniesAuthFilter] and customers will be inheriting from CustomersBaseController with [CustomersAuthFilter].

MVC Display Authenticated User Information on Authenticated pages

I have an ASPX MVC 4 C# web application that has user forms authentication. Once the user logs in, I would like to display a Welcome [user] message on any authenticated page. I know that User.Identity.Name pulls the user name, but I want to grab info from an additional field which would require a custom query.
How do I go about displaying the Welcome message on all pages?
I made an attempt at using a Partial file, but that got me no where. This should be one of the easier things... to variably pass a string onto every page based off logged in user, but from my searching it is not.
I would normally provide some of my code but I'm not sure there is really much to show, more or less I need a pointer or good example in the right direction.
Much appreciated
To get additional fields on a user object you can use the following:
Membership.GetUser(UserName)
and stores the message in a viewbag which you can use on all you views.
You can store the information in Session, screw ViewBag. You can set the Session Propert in the Global.asax file. You should see and OnSessionStart method inside the Global.asax.
So you can say
protected void Session_OnStart()
{
//Whatever is defaulted here
System.Web.HttpContext.Current.Session["blah"] = "Your User Name"
}
and then in the Shared Layout folder _Layout which is the default "Master Page" if you wanna call it that. You can call it like this whereever you like
#String.Format("{0}",System.Web.HttpContext.Current.Session["blah"]);
Edit:
An easy way you can use session variables is to create a Session variable class.
namespace YourSession
{
public static class SessionProperties
{
public static UserAccount UserAccountx
{
get
{
return (UserAccount)HttpContext.Current.Session["UserAccount"];
}
set
{
HttpContext.Current.Session["UserAccount"] = value;
}
}
}
}
And then in your onStart() method you can say
YourSession.SessionProperties.UserAccountx = "Get User Account Method or w.e";
Then in your view it would be
#String.Format("{0}",YourSession.SessionProperties.UserAccountx);
Although C Sharper pointed out a potential solution, that at first I thought worked, I decided it wasn't the exact solution I was looking for. Reason being if I logged out and then back in as a new user in the same session, the session was not updating as the session was already loaded.
Here is what I did:
Layout Master File
<% if (Request.IsAuthenticated) { %>
<div class="welcome">Welcome, <%: Html.Action( "GetUserInfo", "Member" ) %></div>
<% } %>
GetUserInfo is an ActionResult within the Member Controller
Member Controller
public ActionResult GetUserInfo()
{
string userInfo = "";
using (EntityObject db = new EntityObject())
{
var account = db.table_name.FirstOrDefault(u => u.UserID == User.Identity.Name);
userInfo = account.UserDataToDisplay;
}
return Content(userInfo);
}
*I did change actual item names to be more generic for description purposes.
Upon doing this it worked exactly as I wanted. I have one method under one controller, and one line of code on the master page that upon a user being authenticated, displays the relevant information.
Simple and Easy. Just took a while to figure it out.
Well you can use Cookies instead, they work same as session just they do not overload the server side. you can make them go expire when desired as well

MVC Equivalent of Page_Load

I have a session variable that is set in my MVC application. Whenever that session expires and the user tries to refresh the page that they're on, the page will throw an error because the session is not set anymore.
Is there anywhere I can check to see if the session is set before loading a view? Perhaps putting something inside the Global.asax file?
I could do something like this at the beginning of EVERY ActionResult.
public ActionResult ViewRecord()
{
if (MyClass.SessionName == null)
{
return View("Home");
}
else
{
//do something with the session variable
}
}
Is there any alternative to doing this?
What would the best practice be in this case?
If it's in one controller, you can do this:
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
... // do your magic
}
It will fire before on any action execution. You can't return a view from there though, you'll have to redirect to anything that returns action result, e.g:
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary { { "controller", "Shared" }, { "action", "Home" } });
But, obviously, that should redirect to the action in the controller that's not affected by the override, otherwise you have a circular redirect. :)
First, you should redirect to Home, not return the Home View, otherwise you have the weird situation of the home page showing up despite the Url being somewhere else.
Second, Session will never be Null, because a new session gets created when the old one expires or is reset. You would instead check for your variable and if THAT is null, then you know the session is new.
Third, If you app depends on this session data, then I would not use a session at all. Are you using this to cache data? If so, then using Cache may be a better choice (your app gets notified when cache items expire).
Unfortunately, this is probably a case of The XY Problem. You have a problem, and you believe Session solves your problem, but you're running into a different problem with Session, so you are asking how to solve your session problem rather than how to solve the problem Session is trying to solve.
What is the real problem you are trying to solve with this?
EDIT:
Based on your comment below, why don't you pass the customer number on the url:
http://website/Controller/ViewRecord/3
public ActionResult ViewRecord(int id)
{
// do whatever you need to do with the customer ID
}

How can I redirect a site based on date in ASP.NET MVC?

I'm trying to find the best way to redirect a user to a page based on the current date. Exactly What I'm trying to accomplish is in the code below.
DateTime Today = DateTime.Now;
DateTime LaunchDate = DateTime.Parse("17/06/11");
DateTime CloseDate = DateTime.Parse("19/06/11");
int isClosed = DateTime.Compare(CloseDate, Today);
int isOpen = DateTime.Compare(LaunchDate, Today);
if (isClosed < 0){
return RedirectToAction("Closed", "Home");
}
else if (isOpen > 0){
return RedirectToAction("Index", "Home");
}
else{
return RedirectToAction("ComingSoon", "Home");
}
Where in the global.asax(or is it even possible) would this condition go?
I would put that code in a custom MvcHandler.
You could put it in ActionFilter, but then you would have to apply it to all actions.
Here's the code I use for a similar requirement, with a couple of additional features to make testing easier. It could be set up as a global filter, though I prefer to apply it to controllers/actions individually so that specific pages can be available before launch.
Note that this returns a ViewResult rather than a RedirectResult - this way the original URL is maintained, which means that if someone with the right role logs in from the placeholder page they can be redirected to the URL they originally requested.
public sealed class PreviewAuthAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
// todo: if site is live, show page
if (DataStore.Instance.Settings.Get("islive") == "True") return;
// if request is from localhost or build server, show page
if (filterContext.HttpContext.Request.IsLocal) return;
if (filterContext.HttpContext.Request.UserHostAddress.StartsWith("192.168.0")) return;
// if user has has beta role, show page
if (filterContext.HttpContext.Request.IsAuthenticated && (filterContext.HttpContext.User.IsInRole("Beta"))) return;
// site is not live and user does not have access - show placeholder
filterContext.Result = new ViewResult()
{
ViewName="Placeholder",
ViewData = filterContext.Controller.ViewData,
TempData = filterContext.Controller.TempData
};
}
}
I would not do this in global.asax, although you could set this up to figure out the routes you have registered. Assumes that the entire "site" will be coming soon, open and closed. The problem with this method, if it works in your case, is someone can circumvent by playing around. Ouch!
As I was typing, Jakub popped up with Handler, which is a good option. You can set it so no pages can be seen other than the one you desire, which is what it sounds like you want.
You could create custom RouteConstraints by implementing the IRouteConstraint. Within the match method you could add the logic for the datetimes you want to check. This would require you to have multiple routes in your routetable. which all refer to different controllers/actions where you can show a different View to the user.
Below url shows lots of samples on how to implement a custom RouteConstraint.
http://stephenwalther.com/blog/archive/2008/08/07/asp-net-mvc-tip-30-create-custom-route-constraints.aspx

How to set a default initialization cookie in ASP.NET MVC

I was wondering if anyone can shed some light on cookie management. More precisely I would like to know how I could set up an initial cookie/s when a user start a session in a website.
Currently the ASP.NET_SessionId cookie is located on user computers when they navigate to the domain. I would like to set up an additional cookie with details of languageid and countryid with default parameters the first time the user navigate to the site.
Do anyone knows if there is any technique to do this, such as through web.config set up or placing cookie details using layout.cshtml like
Response.Cookies["language"].Value = "1";
Response.Cookies["country"].Value= "7";
or similar?, any option will be appreciated.
You could do this in an action filter:
public class LocalizationAwareAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var httpContext = filterContext.HttpContext.Current;
if (!httpContext.Cookies.Keys.Contains("language"))
{
httpContext.Response.AppendCookie(new HttpCookie("language", 1));
}
if (!httpContext.Cookies.Keys.Contains("country"))
{
httpContext.Response.AppendCookie(new HttpCookie("country", 7));
}
}
}
The filter can be applied globally, so you don't have to worry about remembering it on each action method or controller.
I haven't worked much with cookies in ASP.NET MVC, but the way I'd do it would be to have a little block of code in Global.asax or a separate base controller that gets executed on every request. This code would check the HttpContext for the existence of such a cookie, and if it didn't exist, it would create one. I'll work up a code sample and will update this answer soon.

Categories