Store ASP.NET Site Visitor Stats in MongoDb

By Peter Bromberg

How to use MongoDb with an ASHX handler to capture site visitor statistics including GEOIP Info

This is my 4th article about MongoDb in as many weeks, and for good reason. The more I use MongoDb (and in particular the excellent NoRm MongoDb C# driver) the happier I’ve been. In a nutshell, it just rocks! You'll be saying, "Look, Ma! No SQL!" in no time at all.

If you want to keep up with recent history, I’d recommend reading these articles in order before attacking this one. I think they’re all good and will be helpful if you are just starting out:

http://www.eggheadcafe.com/tutorials/aspnet/63de8012-127a-4478-8725-3e1c27969596/nosql-mongodb-install-lotus-notes-and-couchdb.aspx

http://www.eggheadcafe.com/tutorials/aspnet/51d3ae19-d6f9-4807-ac0a-0baab2964b03/mongodb-install-as-service-and-use-net-drivers.aspx

http://www.eggheadcafe.com/tutorials/aspnet/93206c89-09c9-40fc-9296-7d74bb7996ad/a-mongodb-cache-utility.aspx

MongoDb is easy to install (with a couple of gotchas that I’ve already covered, above), it’s very fast, it uses little resources, and best of all, with the NoRm C# driver and possibly one or two of the other C# drivers, you can completely eliminate the necessity for ORM’s such as NHibernate, LINQ to SQL, or Entity Framework, since you can store your “junk” right in the “trunk” as .NET OOP objects with no mapping layer at all. There is no schema, no mapping. There’s no such thing as a “Table”. Everything is a “Document”. You create a POCO object which can include List<T>’s of sub-objects, etc. and MongoDb will happily store all of it, allow you to index it, and let you query it back out in style, including queries based on sub-objects. And it will do it so fast, you’ll be amazed.

You create an object hierarchy; a Domain Model of classes, and you can persist the whole thing – as is – with about one line of code. And you can search and query it back out with familiar LINQ semantics, again, usually in one or two lines of code. If you want to get fancy, you can even use Map->Reduce, and you can even store very large objects via the GridFs paradigm. All of this is in the NoRm driver, and there are over 400 unit tests whose code you can easily study to see how it is done.

Since this thing originated in the LAMP camp, it’s already in use by many large production sites. The .NET Drivers simply make the Win32 port of MongoDb easy to use for us .NET developers; the learning curve can be measured in just hours or days.

If you believe in the concept of “Less is More” as I do, this can be a very refreshing and rewarding experience. You can build just about anything via MongoDb that doesn’t rely on transactions – and there are, in many cases, workarounds for that too.

One of the simplest scenarios where you can “convince yourself” to start using MongoDb is a situation where you would like to perform some function for your web site that doesn’t have to put additional stress on your primary RDBMS – whether that be MySQL, Oracle, SQL Server, or another.

Storing web site visitor statistics, and to be able to query and report on them, is one of these scenarios. So let’s look at a sample implementation that uses MongoDb to store visitor stats, including retrieved GEOIP information, using an ASHX handler to do the “stuff”.

First, you need to install MongoDb as a service. To do that, follow the simple 5-step process I’ve highlighted in this article.

When you’re done, come back here and we’ll continue.

Now that MongoDb is installed and the service is running, let’s write some code.

We’re going to start out with a class that will serve as a container for a set of visitor stats:

[Serializable]
public class Stat
{
[MongoIdentifier]
public Guid _id {get;set;}
public string Type {get;set;}
public string Browser {get;set;}
public string Version {get;set;}
public string Platform {get;set;}
public string UrlReferrer {get;set;}
public string UserHostAddress {get;set;}
public bool IsAuthenticated {get;set;}
public string HttpMethod {get;set;}
public DateTime LogDateTime { get; set; }
public string City { get; set; }
public string State { get; set;}
public string Country { get; set; }
public string Latitude { get; set; }
public string Longitude { get; set; }
}

I also have a “Helper class that is basically lifted out of the NoRm driver test suite, and simplified.
Next, we need an ASHX Handler; what we will do is to insert an <img> tag in our MasterPage head area that will request an image from this handler, like this:

<img id="stats" src="a.ashx?name=a.gif" />

In this manner, any page that is requested will have a request to our a.ashx handler, which will record the statistics and serve a one-pixel gif.

Here’s the code that handles saving a page “visit”:

public class a : IHttpHandler
{
public void ProcessRequest(HttpContext ctx)
{
HttpBrowserCapabilities bc = ctx.Request.Browser;
Stat stat = new Stat();
stat._id = Guid.NewGuid();
stat.Browser = bc.Browser;
stat.Type = bc.Type;
stat.Version = bc.Version;
stat.Platform = bc.Platform;
stat.UrlReferrer = ctx.Request.UrlReferrer.ToString();
stat.UserHostAddress = ctx.Request.UserHostAddress;
stat.HttpMethod = ctx.Request.HttpMethod;
stat.IsAuthenticated = ctx.Request.IsAuthenticated;
stat.LogDateTime = DateTime.Now.ToLocalTime();

WebClient wc=new WebClient();
try
{
string s =
wc.DownloadString("http://ipinfodb.com/ip_query.php?ip=" + stat.UserHostAddress + "&output=xml");
wc.Dispose();
XmlDocument doc = new XmlDocument();
doc.LoadXml(s);
stat.Country = doc.DocumentElement.SelectNodes("CountryCode")[0].InnerText;
stat.State = doc.DocumentElement.SelectNodes("RegionName")[0].InnerText;
stat.City = doc.DocumentElement.SelectNodes("City")[0].InnerText;
stat.Latitude = doc.DocumentElement.SelectNodes("Latitude")[0].InnerText;
stat.Longitude = doc.DocumentElement.SelectNodes("Longitude")[0].InnerText;

/* Could also do this:
  XElement doc = XElement.Parse(s);
                stat.Country = (string) doc.Element("CountryCode");
                stat.State = (string)doc.Element("RegionName");
                stat.City = (string)doc.Element("City");
                stat.Latitude = (string)doc.Element("Latitude");
                stat.Longitude  = (string)doc.Element("Longitude");  
*/

}
catch(Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message+ ex.StackTrace );
}
finally
{
wc.Dispose();
}
using (Mongo mongo = Mongo.Create(Helper.ConnectionString() ))
{
MongoCollection<Stat> coll = (MongoCollection<Stat>)mongo.GetCollection<Stat>();
coll.Save(stat);
}

string sFileName = String.Empty;
string sPath = ctx.Server.MapPath(".");
try
{
sFileName = ctx.Request["name"].ToString().Trim();
if (sFileName.Length < 5) { return; } // must be at least "1.gif" (5 chars)
// serve the image that was requested:
ctx.Response.WriteFile(sPath + @"\" + sFileName);
}
catch (Exception e)
{

ctx.Response.Write(e.Message);
}
}
public bool IsReusable { get { return true; } }
}

You can see above that I am using the HttpBrowserCapabilities class to get basic info, along with the Request context to get more info. Finally, I make an API call to ipinfodb.com to get city, state, longitude and latitude of the visitor.

Then the handler saves the class instance to my MongoDb:

using (Mongo mongo = Mongo.Create(Helper.ConnectionString() ))
{
MongoCollection<Stat> coll = (MongoCollection<Stat>)mongo.GetCollection<Stat>();
coll.Save(stat);
}

And finally, it serves the image that was requested:

sFileName = ctx.Request["name"].ToString().Trim();
if (sFileName.Length < 5) { return; } // must be at least "1.gif" (5 chars)
// serve the image that was requested:
ctx.Response.WriteFile(sPath + @"\" + sFileName);

There is another page that can be used to view results via this code:

using (Mongo mongo = Mongo.Create(Helper.ConnectionString()))
{
MongoCollection<Stat> coll = (MongoCollection<Stat>)mongo.GetCollection<Stat>();
this.GridView1.DataSource = coll.AsQueryable().OrderByDescending(s=>s.LogDateTime ).ToList();
GridView1.DataBind();
}

Lastly, I have a CreateIndex page that calls this code in my Helper class:

public static void CreateIndex()
{
using (Mongo mongo = Mongo.Create(Helper.ConnectionString()))
{
MongoCollection<Stat > coll = (MongoCollection<Stat >)mongo.GetCollection<Stat>();
coll.CreateIndex(u => u._id,"id",true, IndexOption.Ascending);
coll.CreateIndex(u => u.LogDateTime, "LogDateTime", false ,IndexOption.Ascending);

}
}


You can download the full Visual Studio 2010 solution here.



Popularity  (2289 Views)
Picture
Biography - Peter Bromberg
Peter Bromberg is a C# MVP, MCP, and .NET expert who has worked in banking, financial and telephony for over 20 years. Pete focuses exclusively on the .NET Platform, and currently develops SOA and other .NET applications for a Fortune 500 clientele. Peter enjoys producing digital photo collage with Maya,playing jazz flute, the beach, and fine wines. You can view Peter's UnBlog and IttyUrl sites. Follow Microsoft MVP
Create New Account
Article Discussion: Store ASP.NET Site Visitor Stats in MongoDb
Peter Bromberg posted at Friday, June 25, 2010 11:51 AM
reply