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.