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.