
Introduction
The purpose of this article is to show how to create a more specialized DropDownList web control - specifically one to be used for displaying Countries - suitable for use on ASP.NET Web Forms. The main advantage of this is (not only that it displays all countries, adding each one in HTML could be pretty tedious) but it will also automatically recognize the country that the visitor is from, well, the country the person is visiting the website from.
The web control uses a database from MaxMind, specifically its GeoIP Free Database. MaxMind also provide a C# class to access its database.
It's a very simple control, and the bulk of the work is performed by the ready-prepared CountryLookup class so the best way to go through it is provide an overview of how it works, and then the implementation details.
As ever, a live demo is available on my website.
How it works
Broadly, the CountryListBox web control derives from DropDownList and adds the contents of an ASCII text file into the CountryListBox's Items collection (a list of countries). MaxMind provides a CountryLookup class and within that class is an array of strings for each country - a utility program was created to dump the contents of the this array into an ASCII text file. Part of the array includes "N/A" and "Anonymous Proxy", these were removed from the text file. This text file is included in both the archives so you don't have to worry about re-creating this.
After the countries have been loaded a lookup is performed against the IP database to determine the location of the current visitor, this is then selected in the drop down list box - allowing users to correct it if it's wrong. Aside from that it behaves as any other DropDownList control would.
Now the technical details behind MaxMind's GeoIP database (not strictly necessary to understand since the C# class is provided by MaxMind).
MaxMind store IP address ranges, each of these records is also noted against a country. For example (in CSV format), "1029177344","1029439487","AU","Australia". The IP address is calculated as ipnum = 16777216*w + 65536*x + 256*y + z. For example, to compute the IP Number based on 24.24.24.24: 404232216 = 16777216*24 + 65536*24 + 256*24 + 24. This is all handled courtesy of the CountryLookup class -- which is available to download direct from MaxMind (but is included in the CountryListBox assembly).
Implementation details
The control is relatively simple in design, the majority of the work is performed in the OnInit override. The purpose of which is to add all the countries to the control's Items collection, and then select the one the visitor is from.
OnInit
Below is part of the code for the OnInit method. It includes code to determine whether the Application Cache should be used to store the Geo IP Database (this is set through the CountryListBox's CacheDatabase property. If it is to use the cache, and detects that the data is not already stored, it loads the file into a MemoryStream object (this is performed through the FileToMemory static method - this was added to the CountryLookup class by me). This MemoryStream object is then stored in the Application Cache.
if (useAppCache)
{
if (Context.Cache.Get("GeoIPData") == null)
Context.Cache.Insert("GeoIPData",
CountryLookup.FileToMemory(
ConfigurationSettings.AppSettings["GeoDatFile"]),
new CacheDependency
(ConfigurationSettings.AppSettings["GeoDatFile"]));
The LoadCountries method is then called to populate the Items collection and a lookup is performed, and the matching country selected.
LoadCountries();
CountryLookup cl = new CountryLookup(
((MemoryStream)Context.Cache.Get("GeoIPData"))
);
string visitorCountry = cl.lookupCountryName(
this.Page.Request.ServerVariables["REMOTE_ADDR"]
);
this.SelectedIndex = this.Items.IndexOf(
new ListItem(visitorCountry,visitorCountry)
);
}
CountryLookup changes
The CountryLookup class was provided by MaxMind, and so a few changes were necessary to enable the search to be performed against a database stored in memory. This turned out to be pretty easy thanks to .NET's stream infrastructure.
Deploying the demo
Within the demo is a test Web Form, the GeoIP database, the Countries text file and the CountryListBox assembly. The directory structure is ready to copy, but the contents of web.config will have to be changed to reflect the different paths. You're free to place these files anywhere (i.e. outside of the publicly accessible file structure), just ensure that the ASP.NET User account has the necessary access rights.
You'll have to do a few basic things to add the control to your page,
- Add the import statement to the top of the page:
<%@ Register TagPrefix="etier" Namespace="Etier"
Assembly="CountryListBox" %>
- Then add the tag to the page, it should support any of the standard
DropDownList properties (although I haven't tested these thoroughly :)) <etier:CountryListBox
Id="MyListBox"
RunAt="server"
CacheDatabase=true
CacheCountries=true
/>
Performance
I performed some limited testing on the control to see how it performed when under load. I use Windows XP Professional as my development platform which has a restricted version of IIS - limited to 10 simultaneous connections. I used Microsoft Application Center Test to perform the testing, which involved loading the demonstration page as many times as possible over 5 minutes.
Non-cached version results
The graph below shows the results of the test using the standard control (without any caching). Each time a page is requested the GeoIP Database file is loaded and searched, the countries are also loaded from the text file. Below the graph are some basic statistics that were recorded during the test.

Average requests per second: 32.47
Average time to first byte (msecs): 279.39
Average time to last byte (msecs): 279.55
Response Code: 403 - The server understood the request, but is refusing
to fulfill it.
Count: 5,429
Percent (%): 55.73
Response Code: 200 - The request completed successfully.
Count: 4,313
Percent (%): 44.27
Since my local machine is not really designed to be a server (it's an Athlon XP 2000+ based machine, with 512MB of RAM but without any SCSI hard disks) it's fair to assume that a dedicated server would perform better. Despite this, the server could only sustain an average of 32 requests a second.
Cached results
To improve performance I used ASP.NET's Application Cache to store both the GeoIP Database and the countries. The option to use both of these can be set through the CacheDatabase and CacheCountries properties. I then ran the exam same test script, the graph of the results below shows quite a dramatic difference (far greater than I expected). Again, some basic statistics are included below.

Average requests per second: 146.01
Average time to first byte (msecs): 43.48
Average time to last byte (msecs): 43.76
Response Code: 403 - The server understood the request, but is refusing
to fulfill it.
Count: 6
Percent (%): 0.01
Response Code: 200 - The request completed successfully.
Count: 43,797
Percent (%): 99.99
As a result of caching the number of requests that can be handled per second has increased from 32 to 146, an increase of around 350%.
Conclusion
Thanks has to go to Per Soderlind for writing an article that first told me about MaxMind's GeoIP database. Thanks must also go to MaxMind for providing the database free of charge. It appears that its free of charge to commercial applications also -- but commercial services are also offered that include details on region, state and even NetBlock owners!
It's an extremely simple control to create, but one that is extremely useful! It should hopefully prevent quite so many people wrongly filling out forms. Once again, feel free to E-mail me if you have any comments or questions, alternatively post a message below.
| You must Sign In to use this message board. |
|
|
 |
 | Status  Ralph Mullenders | 6:09 23 Jul '07 |
|
 |
Hi there,
What is the current status for this control?? I have a random 'problem' with a null object reference. I don't know exactly what is causing this.
Besides that I would like use the two character country codes as well.
Thanks, Ralph
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Sorry to anybody trying to see the control working in action, due to a mix-up between me and my hosting company (in my opinion more their fault than mine ;P) my domain wasn't renewed and my site is now down.
Anyway, I'm using this to spur myself into a bit of action so I'm moving hosts and I'll be writing updated articles for all of my stuff that I've been meaning to do.
Sorry for the inconvenience, but in return you'll get some updated material to get your teeth into
I should also mention the primary domain will be oobaloo.com for both the website and email, but I'll post a message once all the domain name stuff has been sorted. Thanks again.
-- Paul "Put the key of despair into the lock of apathy. Turn the knob of mediocrity slowly and open the gates of despondency - welcome to a day in the average office." - David Brent, from "The Office"
MS Messenger: paul@oobaloo.co.uk Download my PGP public key
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
IPAddress.Address Property member is now obsolete.
N.B: This namespace, class, or member is supported only in version 1.1 of the .NET Framework.
Replace
byte[] b = BitConverter.GetBytes(addr.Address);
with
byte[] b = addr.GetAddressBytes();
Thanks
|
| Sign In·View Thread·PermaLink | 5.00/5 |
|
|
|
 |
|
|
 |
|
 |
The DAT file is the GeoIP database from MaxMind that lets the control lookup IP addresses to determine country. The source code contains the C# code to read the database so you can get an idea from that about how it works, the only difference was I added some code to maintain the DB inside the cache and read through a MemoryStream object.
-- Paul "Put the key of despair into the lock of apathy. Turn the knob of mediocrity slowly and open the gates of despondency - welcome to a day in the average office." - David Brent, from "The Office"
MS Messenger: paul@oobaloo.co.uk Download my PGP public key
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
why the page showing the error below?
Access to the path "C:\inetpub\wwwroot\CountryListBox\bin\GeoIP.dat" is denied.
|
| Sign In·View Thread·PermaLink | 4.00/5 |
|
|
|
 |
|
|
 |
|
 |
Hi,
Real nice control - Thanks. Would be nice to have a way of getting the 2 digit Country code back from the listbox. Looks like it's in the origional code but not ported to the asp control. Seems both the key and text are set to the same value, the full name.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Geez, sorry for taking way too long to answer this (although this isn't really an answer )
I'll try and get the 2 digit country code into the control ASAP, and you're right, the key and vale are both set to the full country name.
Sorry I don't really have more to add -- I've got a big report for University to be writing, I'm deep in MSIL and Interop. at the moment
-- Paul "Put the key of despair into the lock of apathy. Turn the knob of mediocrity slowly and open the gates of despondency - welcome to a day in the average office." - David Brent, from "The Office"
MS Messenger: paul@oobaloo.co.uk Sonork: 100.22446
|
| Sign In·View Thread·PermaLink | 1.67/5 |
|
|
|
 |
|
 |
This control is really very cool, but I've got one problem: I have a page where users can register to a service, where I use this control as Country-list. When a user presses "send", het gets the same page, with on top a label saying "Data is added to the DB", and the "send"-button disabled. When a user now changed the country while entering his details, and now gets the same page, he gets the first country again, so he could think he made a mistake? Do you understand? Sorry for my poor english...
Thanks, Ikke
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hmmmm... it seems that postback isn't working as it should -- I'll look into it now and get back to you asap, it's probably possible to 'hack' it by putting in a check for IsPostBack somewhere in the OnInit method (so only populate the control outside of a postback).
I'll hopefully get something before the start of next week.
-- Paul "If you can keep your head when all around you have lost theirs, then you probably haven't understood the seriousness of the situation." - David Brent, from "The Office"
MS Messenger: paul@oobaloo.co.uk Sonork: 100.22446
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Thanks for the quick reply! Your control is really usefull, because it adds a sort of personalisation to the site, just as if the user's country is the default, so it's the most important for you (LOL)
Thanks!!!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
It's actually just a file on the hard disk, so I would imagine it will be pretty much limited by the I/O speed. I'll try and get some kind of test going with my local machine using some of the other PC's in Halls as clients.
Do you know whether there's any kind of behind-the-scenes caching with regards to Disk I/O? I'll post a question in the testing forum and see if I can get any suggestions about methodologies etc.
I suppose one way of optimising it (if it does turn out to be too disk intensive) would be to store the GeoIP database in some kind of memory stream in the application cache, and then read from that. I'll try and do something this Friday or over the Weekend and get either an update, or a whole other article on it.
I've been meaning to do testing on loads of these ASP.NET controls
-- Paul "If you can keep your head when all around you have lost theirs, then you probably haven't understood the seriousness of the situation." - David Brent, from "The Office"
MS Messenger: paul@oobaloo.co.uk Sonork: 100.22446
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I've done some (very) rudimentary testing on my local machine (Windows XP Professional, 512 MB RAM, IDE Hard Drive) -- I unfortunately don't have access to a Windows 2000 Server machine.
I tweaked the code to store both the GeoIP database and the list of countries in the Application Cache, both with dependencies on their underlying files.
Tests were run for 5 minutes, with 10 simultaneous connections.
Results: Average time to last byte (msecs): 279.55 (no-cache) Average time to last byte (msecs): 43.76 (cached)
Average requests per second: 32.47 (no-cache) Average requests per second: 146.01 (cached)
Average bandwidth (bytes/sec): 298,437.92 (no-cache) Average bandwidth (bytes/sec): 2,201,473.45 (cached)
The non-cached page received the 403 - Server Understood but is refusing to fulfil the request error message 55.73% times. On the cached page this was received 6 times - 0.01%
--------------
The result is pretty clear Even if the testing was less than ideal!
The wideness of the results can probably be explained by the slow speed of my single IDE hard disk, as opposed to memory. I would imagine on a proper server setup (SCSI RAID'ed disks) that it would be less of an issue, even so, performance is still significantly improved.
I'll start cleaning up the caching code and prepare an updated article to be posted. In the meantime, if anybody just wants the raw project feel free to ask.
-- Paul "If you can keep your head when all around you have lost theirs, then you probably haven't understood the seriousness of the situation." - David Brent, from "The Office"
MS Messenger: paul@oobaloo.co.uk Sonork: 100.22446
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
Why would a RAID setup improve your performance results? We are talking about a 600KB data file. This is probably in the filecache all the time. It sure is when doing a performance test 
How did you do the test? Use random IP's?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Ok, maybe a RAID setup wouldn't improve it, as you say, 600k is pretty small My bad.
As for the tests, I only had two IPs I could use (my SSH tunnel connection to the US, and my local one). However, since the IP look-up code provided by MaxMind didn't seem to have any caching support, it would perform the lookup for each IP. Also, since I was only really interested in a comparison between the two approaches, I was only after the relative difference and so provided I did the same in each test I wasn't too concerned.
As I said, I don't really have the resources to perform any kind of proper test (only my local machine), but it'd be great to hear from anyone if they have decided to really put the code to the test.
-- Paul "Put the key of despair into the lock of apathy. Turn the knob of mediocrity slowly and open the gates of despondency - welcome to a day in the average office." - David Brent, from "The Office"
MS Messenger: paul@oobaloo.co.uk Download my PGP public key
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
SCSI would be faster than IDE in access time for general disk IO operations -- that is a fact. If you are not caching the file then this would be, although a slight one, a factor in performance. But it is obvious that caching it in memory, and likewise, accessing it from memory, would be much faster than disk IO operations and obviously would not be affected by disk IO, so SCSI is pointless except for when the cachdependency object has changed(when the file is changed -- almost never) or the file is first loaded into memory. Since inserting into the ASP.Net Cache is inserting into memory, we know that by doing so, it is going to be faster. Maybe I missed something, but it is simple: use caching, unless you are worried that 600K is too much to allocate of your RAM, and if you are, maybe you should be looking for a good laxative because something is obviously stuck in your cornhole - you are way too uptight for your own good.
But enough of that. I am responding partially to tell you that the almost exact same control is used in http://www.dotnetnuke.com[^], a free, largely used, hugely scalable, open-sourced .Net portal software that is loaded with a lot of coding goodies. In fact they use the exact same classes and everything, although theirs is rewritten in VB.Net. You should take a look. If anything, you can see how they implemented it and its performance in comparison to yours.
cheers
----------------------- I pity the foo.
-Mr. T
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
 |
|
 |
Not forgetting that they also provide a secure tunneling service (SSH) that allows you to use almost any Internet service through an encrypted connection to their servers.
-- Paul "If you can keep your head when all around you have lost theirs, then you probably haven't understood the seriousness of the situation." - David Brent, from "The Office"
MS Messenger: paul@oobaloo.co.uk Sonork: 100.22446
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Same article, but asp: http://www.codeproject.com/useritems/geoip.asp
"of all the things I've lost, I miss my mind the most" -Ozzy Ozbourne
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Paul has created an ASP.NET control of the same idea, which isn't really the same thing, and IMO deserved a new article rather than simply "adding" it to the other one.
David Wulff Born and Bred.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|