Scott Hanselman

The Weekly Source Code 37 - Geolocation/Geotargeting (Reverse IP Address Lookup) in ASP.NET MVC made easy

November 27, 2008 Comment on this post [24] Posted in ASP.NET | ASP.NET MVC | Source Code
Sponsored By

First, let me remind you that in my new ongoing quest to read source code to be a better developer, Dear Reader, I present to you thirty-seventh in a infinite number of posts of "The Weekly Source Code."

I'm working on a side-project with Rob Conery, Dave Ward and others and I want to be able to do a search immediately as the user arrives on the site, using their location derived from their IP Address. I want to use "Geolocation" or Reverse IP Address Lookup, and take an IP like 127.0.0.1 and turn it into "New York, NY."

There are lots of services and databases that you can buy that will let you make a web service call and get back a location. Some will include latitude and longitude, some include the city name. Some are REALLY expensive, like $500 or more.

I found two solutions, one server-side that's a community project and one client-side from Google!

Community-based Geotargeting from hostip.info

If you hit http://www.hostip.info/ with your browser, it'll guess where you are and show a map with your estimated location, usually within 50-100 miles. It's a community-based project with a freely available database. They've got over 8.6 million entries in their database!

More interestingly, they have a nice clean API for Geotargeted IP Address Lookup.

For example:

http://api.hostip.info/get_html.php?ip=12.215.42.19&position=true
  Country: UNITED STATES (US)
  City: Sugar Grove, IL
  Latitude: 41.7696
  Longitude: -88.4588

if you add just call:

http://api.hostip.info/?ip=12.215.42.19

You'll get an XML document back with lots of good information.

So, I wrote a quick .NET wrapper for this service. Note the sample XML file in the comment in the middle there. I also put in a default location for debugging. It's not the cleanest code in the world, but LINQ to XML made it easy and it either works or it doesn't.

public class LocationInfo
{
public float Latitude { get; set; }
public float Longitude { get; set; }
public string CountryName { get; set; }
public string CountryCode { get; set; }
public string Name { get; set; }
}

public class GeoLocationService
{
public static LocationInfo GetLocationInfo()
{
//TODO: How/where do we refactor this and tidy up the use of Context? This isn't testable.
string ipaddress = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
LocationInfo v = new LocationInfo();

if (ipaddress != "127.0.0.1")
v = GeoLocationService.GetLocationInfo(ipaddress);
else //debug locally
v = new LocationInfo()
{
Name = "Sugar Grove, IL",
CountryCode = "US",
CountryName = "UNITED STATES",
Latitude = 41.7696F,
Longitude = -88.4588F
};
return v;
}

private static Dictionary<string, LocationInfo> cachedIps = new Dictionary<string, LocationInfo>();

public static LocationInfo GetLocationInfo(string ipParam)
{
LocationInfo result = null;
IPAddress i = System.Net.IPAddress.Parse(ipParam);
string ip = i.ToString();
if (!cachedIps.ContainsKey(ip))
{
string r;
using (var w = new WebClient())
{
r = w.DownloadString(String.Format("http://api.hostip.info/?ip={0}&position=true", ip));
}

/*
string r =
@"<?xml version=""1.0"" encoding=""ISO-8859-1"" ?>
<HostipLookupResultSet version=""1.0.0"" xmlns=""http://www.hostip.info/api"" xmlns:gml=""http://www.opengis.net/gml"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:schemaLocation=""http://www.hostip.info/api/hostip-1.0.0.xsd"">
<gml:description>This is the Hostip Lookup Service</gml:description>
<gml:name>hostip</gml:name>
<gml:boundedBy>
<gml:Null>inapplicable</gml:Null>
</gml:boundedBy>
<gml:featureMember>
<Hostip>
<gml:name>Sugar Grove, IL</gml:name>
<countryName>UNITED STATES</countryName>
<countryAbbrev>US</countryAbbrev>
<!-- Co-ordinates are available as lng,lat -->
<ipLocation>
<gml:PointProperty>
<gml:Point srsName=""http://www.opengis.net/gml/srs/epsg.xml#4326"">
<gml:coordinates>-88.4588,41.7696</gml:coordinates>
</gml:Point>
</gml:PointProperty>
</ipLocation>
</Hostip>
</gml:featureMember>
</HostipLookupResultSet>";
*/

var xmlResponse = XDocument.Parse(r);
var gml = (XNamespace)"http://www.opengis.net/gml";
var ns = (XNamespace)"http://www.hostip.info/api";

try
{
result = (from x in xmlResponse.Descendants(ns + "Hostip")
select new LocationInfo
{
CountryCode = x.Element(ns + "countryAbbrev").Value,
CountryName = x.Element(ns + "countryName").Value,
Latitude = float.Parse(x.Descendants(gml + "coordinates").Single().Value.Split(',')[0]),
Longitude = float.Parse(x.Descendants(gml + "coordinates").Single().Value.Split(',')[1]),
Name = x.Element(gml + "name").Value
}).SingleOrDefault();
}
catch (NullReferenceException)
{
//Looks like we didn't get what we expected.
}
if (result != null)
{
cachedIps.Add(ip, result);
}
}
else
{
result = cachedIps[ip];
}
return result;
}
}

I did put some naive caching in here. I would probably put in some cleanup so the cache could only get so big, maybe a few thousand IPs. Perhaps a FIFO queue, where I start yanking the old stuff after it gets full. Or, use the ASP.NET Cache and just let it manage memory and eject stuff that hasn't been touched in awhile.

This worked great for a while, but at some point, if my site became successful I'd want to send these guys money, or regularly download their free Geolocation database and run the whole thing locally.

Then, while looking at Google's Ajax Libraries API site as a place for my site to download the jQuery libraries rather than me hosting them, I noticed…

Free Geotargeting is built into Google's AJAX Libraries API

Google offers a free service called google.load that lets you load your favorite Javascript APIs using Google's Javascript Loader API, rather than hosting them locally. This means that you and your site get a few excellent benefits:

    • The files are loaded fast because they are on Google's CDN (Content Distribution Network)
    • The files are loaded asynchronously by newer browsers because they aren't stored on your site (most browsers open up to 6 HTTP connections per DNS/site, but older ones do 2.
      • This is why back in the 90's we had www.800.com and images.800.com in order to get some parallelizm. It's also recommended by YSlow, although you should balance this knowledge remembering that there's a cost for a DNS lookup. That said, newer browsers like Chrome include their own local DNS cache which mitigates that issue. Moral of the story? Know all this stuff, but know that it'll all be obsolete and moot soon. ;)
    • They offer a versioning API, so you can say, "get me version 2 of something" you'll get 2.24 if it's the latest.

What does this have to do with Geotargeting? Well, you know when you get Google.com they know where you are. They'll offer Spanish if you're in Spain. Since they ALREADY know this stuff, they are making in available to your script via google.loader.ClientLocation for free. They announced this in August and I missed it until today!

I'm doing this:

<script src="http://www.google.com/jsapi?key=YOURAPIKEY" type="text/javascript"></script>
<script>
google.load("jquery", "1.2.6");
google.load("jqueryui", "1.5.2");
var yourLocation = google.loader.ClientLocation.address.city + ", "
+ google.loader.ClientLocation.address.region;
</script>

Note that you need to sign up for a free API KEY for your domain so they can contact you if there's a problem with the script.

Later on, I use the yourLocation JavaScript variable and put it in a Search Box for my app and go searching based on your location. If your location is wrong, I remember if you typed in a different city in the Search Box and I'll use that again, so I only annoy you once.

If that's not enough information, you can use the proper Geolocation API, which is much more sophisticated but requires Google Gears.

Enjoy!

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
November 27, 2008 12:23
This method can get the browser location - but that is only part of the story - how about the user's language. I live and work in France but my working language is English.

There is a JSONP service at http://langdetect.appspot.com/?callback=setLanguage that will detect the user's language choice - see the demo at http://lisbakken.com/lang.html
November 27, 2008 13:17
Hi and thanks for this helpful information.

I have just a question, If I get an e-mail, could I find out where (city or coutnry) the sender is located?

thanks again
November 27, 2008 13:17
Isn't that just the USER_LANGUAGE Http Header?
November 27, 2008 15:51
Thanks for the post Scott, this is a really useful tip. I writing a test application to test MVC features and I think I'll squeeze this in just for fun.
November 27, 2008 15:53
Great article.

If you want a more complete solution, you'll find it in the GeoIP methods of my GoogleMaps control for ASP.NET.

Regards
November 27, 2008 16:38
Hi Scott,

Great Article !

I wrote an article based on yours, in order to make a Provider Based GeoLocation for ASP.NET

If you wants more details check it out here

Best Regards,
November 27, 2008 22:27
Any reason you chose to use WebClient instead of XDocument.Load(uri)?
November 28, 2008 3:04
Hi Scott,

Instead of having to rely on a THIRD PARTY .. why not have a built in .NET component which magically does a pretty good job (and basically follows the same concept as the other 3rd part providers). Also, what happens if this is free AND open source.

I bring you: http://www.codeplex.com/IPAddressExtensions

Enjoy :)

(i'd love to hear if this is good for your project, btw) :)
PK
November 28, 2008 4:34
Try:

if (!HttpContext.Current.Request.IsLocal)
{
///
}

instead of checking 127.0.0.1
November 28, 2008 8:58
Great article. Is there anything similar available from MS?
November 28, 2008 17:44
How is the reliability/accuracy of geolocation these days? Are there still blocks of users that are widely dispersed geographically, but all share a particular range of IP addresses, such that geolocation would report all of those users being from the same location (e.g. AOL users)?

Anecdotal story: Circa 2003, I was working for a company in Ann Arbor, Michigan. We were acquired by a company located in Canada, and our office's access to the Internet was (somehow -- I'm admittedly ignorant of the details) subsequently re-routed to go through the parent company's network. Thereafter, sites using geolocation would report that I was located in Toronto, ON.
November 28, 2008 22:53
Jon,

I've used hostip for quite a while but I've noticed it's not real accurate. It's a good starting point depending on how much accuracy you need.

I needed a map of every user online without using a street address (due to privacy concerns). So I started out using a free zip code database, but users with the same zip code would get stacked on top of each other. Then tried HostIP.info but for some reason users would turn up in rivers and lakes, and the accurace was a 10 mile radius at best. When I tweaked it to use a midpoint of the two I've noticed it's surprisingly accurate (within a few blocks of the actual location for 5 different ISP's in the DC area at least).

I wonder if adding google's service will increase it's accuracy even more though.... I'll let you know.
November 29, 2008 10:01
Kazi - Great tip, thanks!

ScotDWG - I trust Google to be pretty darned accurate. Certainly as accurate as free will get me. ;)

JonS - Agreed. GeoLocation is INCREDIBLY accurate until it's completely INACCURATE. ;)

Yoann - Cool, but I want to get down to the city/town level, not just country.
November 29, 2008 15:48
For the paid Web service such as from IP2Location.com, it actually provides free query up to 90 credits per month. If you are low usage, and required more information than the latitude and longitude, you can apply for the free account at http://www.fraudlabs.com/ip2location.aspx
Tim
November 29, 2008 22:20
Really valuable topic, valuable suggestions.

I will further try these, and update if any new things. thanks
November 30, 2008 23:28
Hi Scott,

First of all, I'm a big fan of your blog and podcasts :)

--

regarding this subject, why not get the RIPE info of the IP?

try this: http://www.balexandre.com/iplookup.aspx

I get for my IP this information:

Country: DK

ISP: CYBERCITY-DSL-USERS [ Cybercity A/S xDSL users ]


there is more info that I can get, but to redirect the user to the country language for example, the RIPE is more than enough and it is for free.

This is the approach that I am using in one website.
December 01, 2008 18:46
I am in Overland Park, KS, and hostip.info is quite certain I'm in Orlando, FL. Great service they've got going! :-)
December 01, 2008 18:50
I should have added: I've used MaxMind GeoIP Lookup -- which has a less-detailed yet quite accurate free level of service -- for a while with some of my php projects. They have a .NET interface with a free level of service, but I haven't used it yet.
December 01, 2008 23:30
I like the following steps for determining users IP

public static string GetUsersIP()
{
System.Web.HttpContext current = System.Web.HttpContext.Current;
string ipAddress = null;

if ( current.Request.ServerVariables["HTTP_CLIENT_IP"] != null)
ipAddress = current.Request.ServerVariables["HTTP_CLIENT_IP"];

if ( ipAddress == null || ipAddress.Length == 0 || ipAddress == "unknown")
if ( current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != null)
ipAddress = current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

if ( ipAddress == null || ipAddress.Length == 0 || ipAddress == "unknown")
if ( current.Request.ServerVariables["REMOTE_ADDR"] != null)
ipAddress = current.Request.ServerVariables["REMOTE_ADDR"];

if ( ipAddress == null || ipAddress.Length == 0)
ipAddress = "unknown";

return ipAddress;
}
December 02, 2008 20:23
Not relevant to the Geolocation discussion, but since you mentioned Google's AJAX API's I thought I'd inject my two cents. While you did mention three reason's to use Google's AJAX API's (CDN, max_connections, and versioning) you missed what is perhaps the single greatest performance gain to be had by using them. That is if you reference the jQuery library using a script tag pointing to http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.js and then a visitor to your site who has visited any other site that references the same script will not need to download that script. It will be pulled from their browser cache. No DNS lookup, no HTTP request overhead, no worrying about max_connections at all. It's already there waiting for your site to use!

Along the same vein, ideas have been floated to add @hash attributes to script tags to allow browsers to cache based on the hash (rather than the URL) and even potentially ship with popular libraries pre-cached.
December 03, 2008 3:56
Scott - did u check out that codeplex project which has all your stuff in a dll and doesn't need to go outside to an external API to get the result?

The trick is that the dll has a resource file (which is a file with all the results, compressed) which is referenced via LINQ. And it's just an extension method to the OutOfTheBox IPAddress class :)

here's some sample code...


IPAddress ipAddress = new System.Net.IPAddress(0x2414188f); // Some IPv4 address in the USA.
// *Check the the overloads to insert the IP address in question*
string country = ipAddress.Country(); // return value: UNITED STATES
string iso3166TwoLetterCode = ipAddress.Iso3166TwoLetterCode(); // return value: US



pretty simple AND no need to rely on an outside connection/api/etc.

hth.
PK
December 04, 2008 3:37
I think that the main feature of this solution is getting not only country (there is many solutions for this, simple and not) but city too. I didn't see many services that allow to get city location. Thank you, Scott!
December 10, 2008 4:48
Thanks! Adapted it to my needs, worked great.
December 12, 2008 16:54
thanks scott, it is so easy!!!

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.