How to capture page views in MVC - asp.net-mvc

What is the best way to capture the number of page views and update the database in MVC? I have gone through what is the best way to capture page views per user in asp.net-mvc solutions in which using action filters are suggested. But what if we want to track the pageviews for each unique parameter passed to the action method - like how stackoverflow calculates the page visits for each question ? I think, we cannot use action filters in this case as page views differs for different parameters.
Is there any solution available otherthan incrementing the database value each time the page is viewed ?

why can you not use action filters
public class MyActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var actionName = filterContext.ActionDescriptor.ActionName;
var controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
var parameterICareAbout = filterContext.ActionParameters["parameterICareAbout "];
//write to database using parameterICareAbout
}
}
As for not storing value on the database everytime, you could set up a Singleton class in your app that keeps track of all these requests and then bulk writes them to the database at some cadence/ or writes to the database during the application spin down process

Related

Implement my own statistics engine and have a record per website visit?

I am supposed to create an internal statistics mechanism for our ASP.NET MVC 4 web application. We are not going to use external ones like Google Analytics or even Glimpse. Because I'm not sure if I can extract needed data from their API.
What we expect this mechanism is very like to Google Analytics including page hit count, referer, keyword, etc. But just for part of pages not all. We want to use these data in our own pages and reports.
Now I have 2 questions. Is it correct to ignore Google Analytics or Glimpse and implement my own? If yes, it is reasonable to save a record in database per each website visit and then use theses record to extract statistics?
Any help is highly appreciated
I think you can implement both this satistic. Its difficult to say without understanding business logic you need. But if you need more detailed information about every request (visited user roles, retrive controller/action name for some specific statistic, log access to specific resources etc.) you can easily implement this by using action filter.
public class StatisticFilter : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
if (filterContext.IsChildAction) //if action call was from view like #Html.Action do nothing
return;
var CurrentUser = filterContext.RequestContext.HttpContext.User;
if (CurrentUser.IsInRole("some_role"))
return; //write logic for this role
string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
string actionNaem = filterContext.ActionDescriptor.ActionName;
//here the id of the accessed resource - document, sale, page etc.
string id = filterContext.RequestContext.RouteData.Values["id"].ToString();
}
}
Thats all. You can extend this by any logic you need.
In my project i have the statistic table with filds:
Date - timestamp,
Controller - string,
Action - string,
id - bigint
method - string(POST, GET... if post - submited)
user_id - bigint
And insert record for every request executed. So i have most important information about request for any statistic.

Prevent Url Tampering to access another users data

I just wanted to gauge opinions on how I should approach this problem and ultimately looking for a quick win (wrong way to think about things nut time pressures mean I have to think and act quickly!
I've been given a website that has a bit of an issue.
I login using standard forms authentication as User1234 and my url is as follows:
www.mywebsite.co.uk/1234/Contact.
This will take me to User1234's details.
You can put two and two together and correctly assume that 1234 is a user id of some sort.
Once authenticated, I can access the views with [Authorize] attribute present, any anonymous/unathenticated users get redirected.
However, once logged in as User1234, I can then tinker with the url like so:
www.mywebsite.co.uk/1235/Contact.
So I am authenticated as User1234 but can see User1235's data. This is BAD for obvious reasons.
When I log in, I actively set the login ID in session so in theory, I could do a check whenever a user hits an ActionResult, I could cross check the ID present in the URL against the session login ID. However, it is a rather project with lots of action results and as such, I'm reluctant to spend my Saturday afternoon adding something to each and every ActionResult.
Is there an event in the global.asax I could use that is hit on each ActionResult request where I can compare Session login ID with url ID?
Alternatively, can anyone offer some suggestions about how I can achieve this or restrict URL tampering?
You can try and do a base controller
public class BaseController : Controller
{
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
//Do your stuff here
base.OnActionExecuted(filterContext);
}
}
I assume that you don't want to change your URL routes, as you could retrieve the user id also from the session. A quick solution would be to use an ActionFilter which you can place on the affected controllers or action methods:
public class VerifyUserIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var sessionUserId = filterContext.HttpContext.Session["UserId"];
var routeUserId = filterContext.RouteData.Values["UserId"];
if (routeUserId != null && sessionUserId == routeUserId)
filterContext.Result = new RedirectResult("<<url to redirect to>>");
}
}
I don't understand why the URL contains a data entry point. This appears to be a design flaw. I would remove all code that uses a URL parameter and instead make sure the controller looks up what the ID is based on the logged in user.

Working with the Output Cache and other Action Filters

I have added Output Caching to a couple of actions in my app for some easy performance boosts. However, these actions also need to increment a counter after each request (it's a views counter) by hitting a Redis db.
At first, I figured I could just adjust the order in which the action filters execute to ensure the view is counted:
public class CountersAttribute : ActionFilterAttribute
{
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
//increment my counter all clever like
base.OnResultExecuted(filterContext);
}
}
But that didn't work; apparently the OutputCacheAttribute doesn't behave like a normal action filter. Then I tried implementing a custom output cache:
public class OutputCacheWithCountersAttribute : OutputCacheAttribute
{
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
//straight to the source to get my headcount!
base.OnResultExecuted(filterContext);
}
}
Nope, didn't work either; action filters appear to be entirely ignored once an action is cached. Bummer.
So, uh, is there any way (without implementing a custom output caching provider) for me to ensure my views are counted properly that is clean and sensible?
The OutputCacheAttribute has limitations by the way and there is a custom attribute named DonutOutputCache developed by Paul Hiles helps to overcome the limitations.
One of the important feature it supports is you can have an action filter that can be called all the times even the action is marked with cache attribute or not.
For ex. you want to cache an action for the duration 5 seconds and at the same time you want to log every time the action receives a request using a LogThis filter you can achieve that simply by below,
[LogThis]
[DonutOutputCache(Duration=5, Order=100)]
public ActionResult Index()
From Paul,
Yes, unlike the built-in OutputCacheAttribute, the action filters will
execute even when a page is retrieved from the cache. The only caveat
to add is that you do need to be careful about the filter order. If
your action filter implements OnResultExecuting or OnResultExecuted
then these methods will be executed in all cases, but for
OnActionExecuting and OnActionExecuted, they will only be executed if
the filter runs before the DonutOutputCacheAttribute. This is due to
the way that MVC prevents subsequent filters from executing when you
set the filterContext.Result property which is what we need to do for
output caching.
I do not think that you can rely on the order in which action filters
are defined on an action or controller. To ensure that one filter runs
before another, you can make use of the Order property that is present
on all ActionFilterAttribute implementations. Any actions without the
order property set, default to an value of -1, meaning that they will
execute before filters which have an explicit Order value.
Therefore, in your case, you can just add Order=100 to the
DonutOutputCache attribute and all other filters will execute before
the caching filter.
You can make an AJAX call from the Layout View and track your visitors even if the page is cached. This is what Google Analytics does. I recommend doing it from the Layout View because it's gonna be executed in all the view that uses that layout.
One more comment, let's say that you have two Layout Views: one for the public part of the site and one for the back-end (employees only). You'll probably be interested in tracking users, not employees so this is another benefit of tracking at Layout View. If in the future you want to track what the employees are doing, you can add a different tracker for the back-end Layout View.
I hope it helps.
The reason is actually in the .NET source, and nothing to do with the DonutOutputCache:
public void SetCacheability(HttpCacheability cacheability)
{
if (cacheability < HttpCacheability.NoCache || HttpCacheability.ServerAndPrivate < cacheability)
throw new ArgumentOutOfRangeException("cacheability");
if (HttpCachePolicy.s_cacheabilityValues[(int) cacheability] >= HttpCachePolicy.s_cacheabilityValues[(int) this._cacheability])
return;
this.Dirtied();
this._cacheability = cacheability;
}
In other words, if you set NoCache first (a value of 1), it will always return if you try to set a higher value, such as 4 (public).
The only solution is to fork the project and extend it to how you require, or perhaps send a pull request to mark protected ICacheHeadersHelper CacheHeadersHelper in DonutOutputCacheAttribute
Use a "Validation Callback" that is executed ALWAYS even if the cached page should be served
public class MyCacheAttribute : OutputCacheAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
SaveToLog();
httpContext.Response.Cache.AddValidationCallback(MyCallback, null);
base.OnResultExecuting(filterContext);
}
// This method is called each time when cached page is going to be served
private void MyCallback(HttpContext context, object data, ref HttpValidationStatus validationStatus)
{
SaveToLog();
}
}
NOTE: the SaveToLog() is called in two places, that's by design (first call when cache is bypassed, seconds call when cached version is served)

MVC + Multiple tenancy app

So I have an MVC app that should change the Website title, and header color based on the domain the app is hit from. So I have a simple table setup in SQL as such:
DomainName (PK), WebsiteTitle, HeaderColor
Domain1.com, Website Title for Domain 1, #ebebeb
So I am trying to figure out the best way to return this information for each page view. Sure I can go ahead and lookup the site info in each model thats returned from the controller. But are there any other ways I can approach this? Maybe at a lower level in the stack?
Thank you!
There are many ways you can do this. ActionFilters are one way, or in a BaseController.
You need to determine if every action requires this, or if only certain actions.
If you decide every action, create a controller base, inheriting from Controller, then overriding OnActionExecuting. In that method you can make you calls to fetch and add the data to viewdata. Like so:
public class BaseController : Controller
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.Controller.ViewData.Add("SiteTitle", "Site title");
base.OnActionExecuting(filterContext);
}
}
If you prefer to use a base viewmodel that has this information, it would be best to override OnActionExectued where you can get access to the actions results, and modify the base model to set your values. Like so:
public class BaseController : Controller
{
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
var result = filterContext.Result as ViewResultBase;
var baseModel = (BaseViewModel) result.ViewData.Model;
baseModel.SiteTitle = "Site Title";
base.OnActionExecuted(filterContext);
}
}
Depending if you want an inheritence chain for your viewmodels. Either works. You'll also notice that I just set the values. Use whatever source for values you need. If you are pulling them from the db, I would cache the values so that for every action you are not hitting the db for it.
This problem is fundamentally identical to swapping layout or master pages for mobile vs desktop browsers. However, instead of looking at the device caps in a web request to determine which layout to use, you'd check the domain of the request.
See this article for a slightly complex (but thorough) overview of selecting mobile vs desktop views. Much of what the author says is focused on detecting screen solution, etc., which doesn't directly apply to you, but the mechanism for selecting the master or layout page should be just what you're looking for.
Or, you can handle this through inheritance.
Implement a base controller, like so:
public class BaseController : Controller
{
public string SiteTitle { get { .... } }
public string HeaderColor { get { ... } }
/// whatever other "global" properties you need
}
Then, each of your controllers inherit from BaseController
public class HomeController : BaseController
{
public ActionResult Index()
{
var myTitle = SiteTitle;
/// then, do whatever you want with it
return View();
}
}
In the property accessors in BaseController, read the title and whatever other properties you need from a .settings file or the AppSettings section in web.config.
Controller also provides events that can be used to set these properties so that you don't have to duplicate any code for getting those values into each view.

Retrieving the data in every request in ASP.NET MVC

I need to retrieve the data from cookie in every request in ASP.NET MVC and store it in a global variable so that it'll be available throughout the application.
I've two questions here is there any event-handler in ASP.NET MVC where I can get the data from cookie in every request and what kind of global variable I can use to store this cookie value so it is available in all places?
You can use a filter to get the cookie in every request. Create for example a class MyFilter
public class MyFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpCookie cookie = filterContext.HttpContext.Request.Cookies["myCookie"];
//do something with cookie.Value
if (cookie!=null) filterContext.HttpContext.Session["myCookieValue"] = cookie.Value;
// or put it in a static class dictionary ...
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
}
}
Mark every controller class with [MyFilter] attribute.
This example takes the cookie and puts the value in the session so it's available in all the views and all the controllers. Else you can put the cookie value in a static class that contains a static Dictionary and use the session ID as key. There are many way to store the cookie value and access it in every part of the application.
You can use Attributes on each request or make a custom Controller and handle "OnActionExecuting" (override)
You could go old school and handle the onrequest event in the asax file. That way you could abstract the code out to an httpmodule if you need to reuse the approach in another app. The filters approach is probably better though.

Resources