Working with ASP.NET MVC, JSON, and JQuery to show weather

In talking with Jamie today, he was working on (and found a GREAT solution to) working with JSON, from .NET. His example used http://www.wunderground.com/ – which I hadn’t played with in a long time. So, I decided to do something with it.

If you look on http://www.robseder.com, I now have the weather for all the places where I’ve lived in my life and/or where I still keep in touch with people. This works using JQuery to make a call from your browser, to this service to go get the data, and then formats it on the screen.

There are several things that are not straight-forward about this and I clarified a few things in my head, which I wanted to write down.

Wunderground:
If you wanted to pull in weather information to your website, here’s how you might go about it. Weather Underground has a nice simple API that gives you either JSON or XML, and is free to use (for relatively small amounts of requests). Sign up for free and get your API key here: http://www.wunderground.com/weather/api/

Using JQuery to call a service:
One thing I started to recall was that you can’t call a web service that is hosted on another domain – you get an “Access Denied”. That would allow malicious developers to do bad things. So wait a second, how ARE you supposed to do this then? When I tried this, I just get an “Access denied” from the script run-time. After reading up on this a bit, it seems that everyone agrees that you should wrap any external service that you want to use. This… (sigh) … sent me down a very long path. I fun and interesting path, but this did take a while to do.

Crafting a service wrapper:
Ever since I watched the http://www.cleancoders.com/ videos, my brain has started to change. I really question, MUCH more than I used to, pretty much every design decision I make. The downside is that I can spend hours refactoring. I think of it like reducing a math equation:

(128 / 1024) + (64 / 256) = x  (cut it in half)
(64 / 512) + (32 / 128) = x  (hey, this is divisible by 8!)
(8 / 64) + (4 / 16) = x  (wait, I can divide by 4)
(2 / 16) + (1 / 4) = x   (oh, I can solve for x now!)

In programming terms, I will have code that works, but my brain nags at me that it can be simplified. So, I start refactoring – my OCD requires it of me!

SO, with that said – I first realized that I needed:

  • a mechanism for calling the service
  • a mechanism for caching the results
  • a way to give that data to the front-end.

Calling the Wunderground service:
Assuming you went and got yourself an API key, you should be set to go. I created this class as a simple way to offer up this data. You can see this has the actual WebRequest that goes out to the web service to go get the weather data:

/// <summary>
/// Static utility class for getting weather data.
/// </summary>
public static class WeatherServiceProvider
{
/// <summary>
/// The cache provider for weather data.
/// </summary>
private static ServiceCache<String> serviceCache = new InProcServiceCache<String>();

/// <summary>
/// Static constructor.
/// </summary>
static WeatherServiceProvider()
{
ApiKey = ConfigurationManager.AppSettings["wundergroundApiKey"];

TimeSpan cacheDuration = new TimeSpan(0,10,0);

String cacheDurationString = ConfigurationManager.AppSettings["wundergroundCacheDuration"];

if ( !String.IsNullOrEmpty(cacheDurationString))
cacheDuration = TimeSpan.Parse(cacheDurationString);

serviceCache.CacheMiss = GetWeatherFromInternetService;
serviceCache.CacheLifetime = cacheDuration;
}

/// <summary>
/// Gets or sets the current API key to use with the weather service.
/// </summary>
public static String ApiKey { get; set; }

/// <summary>
/// Retrieves a JSON dynamic object, given the specified <paramref name="postalCode"/>.
/// </summary>
/// <exception cref="ArgumentException"/>
public static String GetWeather(String postalCode)
{
if (String.IsNullOrEmpty(postalCode))
throw new ArgumentException("Argument "postalCode" cannot be null or empty.", "postalCode");

return serviceCache.GetResult(postalCode);
}

/// <summary>
/// Calls the actual, underlying service to go get fresh data.
/// </summary>
/// <exception cref="ArgumentException"/>
/// <exception cref="InvalidOperationException"/>
private static ServiceCacheItem<String> GetWeatherFromInternetService(String postalCode)
{
if (String.IsNullOrEmpty(postalCode))
throw new ArgumentException("Argument "postalCode" cannot be null or empty.", "postalCode");

if (String.IsNullOrEmpty(ApiKey))
throw new InvalidOperationException("The 'wundergroundApiKey' value in <appSettings> is missing. Cannot continue. Visit http://www.wunderground.com/weather/api/ to get a key.");

String url = String.Format("http://api.wunderground.com/api/{0}/conditions/q/{1}.json", ApiKey, postalCode);
WebRequest request = WebRequest.Create(url);
using (WebResponse response = request.GetResponse())
{
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
{
return new ServiceCacheItem<String>() { Key = postalCode, Value = reader.ReadToEnd() };
}
}
}
}

This can now be called from the MVC controller:

public void GetWeather(String postalCode)
{
if (String.IsNullOrEmpty(postalCode))
throw new ArgumentException("Argument "postalCode" cannot be null or empty.", "postalCode");

String json = WeatherServiceProvider.GetWeather(postalCode);

Response.Write(json);
}

and finally, this  controller action is consumed in JQuery like this:

<script type="text/javascript">
   1:  

   2:     $().ready(function () {

   3:         getWeather("34607", "sh");

   4:         getWeather("06084", "tl");

   5:         getWeather("90731", "sp");

   6:         getWeather("34667", "hs");

   7:         getWeather("01108", "sf");

   8:     });

   9:  

  10:     function getWeather(postalCode, prefix) {

  11:         $.ajaxSetup({ cache: false });

  12:         $(document).ajaxError(function (e, jqxhr, settings, exception) {

  13:             $("#weatherError").html("Error returning weather data." + exception);

  14:         });

  15:  

  16:         $("#" + prefix + "Conditions").html("Loading...");

  17:  

  18:         $.ajax({

  19:             dataType: 'json',

  20:             url: '/Home/GetWeather?postalCode=' + postalCode,

  21:             success: function (data) {

  22:                 $("#" + prefix + "Image").html("<a href='http://www.wunderground.com/US/" + postalCode + ".html' target='_blank' title='Click to see forecast.'><img src='" + data.current_observation.icon_url + "' /></a>");

  23:                 $("#" + prefix + "LocationName").html(data.current_observation.display_location.full);

  24:                 $("#" + prefix + "LocationName").attr("title", Math.round(data.current_observation.display_location.elevation) + "ft above sea level.");

  25:                 $("#" + prefix + "Conditions").html(data.current_observation.weather + " - <h5>" + Math.round(data.current_observation.temp_f) + "°F</h5> - " + data.current_observation.relative_humidity + " humidity");

  26:                 $("#" + prefix + "Wind").html(data.current_observation.wind_string);

  27:                 $("#" + prefix + "MapLink").attr("href", "https://maps.google.com/?q=" + data.current_observation.display_location.latitude + "," + data.current_observation.display_location.longitude);

  28:             },

  29:             error: function (exception) {

  30:                 $("#weatherError").html("Error returning weather data." + exception);

  31:             }

  32:         });

  33:     }

</script>

assuming you have some HTML like this:

<h4><a id="sfMapLink" target="_blank" title="Click here to view map">
<img src="~/Images/google_maps_icon_24x24.png" align="absmiddle" /></a>&nbsp;
<div id="sfLocationName" style="display: inline;"></div>
</h4>
<div id="sfImage"></div>
<div id="sfConditions"></div>
<div id="sfWind"></div>

So, JQuery makes a request to /Home/GetWeather and passes it a ZIP code. This controller action calls the service provider, which goes out to wunderground and returns JSON. JQuery ultimately gets that back and some simple DOM manipulation outputs it to the screen – again, as seen here: http://www.robseder.com

Caching:
Hold on one second there, partner. What about caching? “Who cares, it works!” you might say. If you are saying that, please stop saying that. 🙂

There are a few CRITICAL reasons to cache this data. First, it is only updated every 15 minutes. Secondly, the web service is kind enough to offer this data for FREE, plus they will charge if you go over modest amounts of traffic. Lastly, your site is going to be slow if you need to go back out to a website every time the page loads. For SO many reasons, and hopefully you agree, you really should cache something like this. But how? Well, this is what I have so far.

First, thinking of reusability, I have a ServiceCacheItem. It is a data structure class that holds an object (of generic type),  a timestamp, and a “key” that uniquely identifies it. Simple enough, right?

/// <summary>
/// Data structure to hold details about a cached item.
/// </summary>
/// <typeparam name="TValue">The data type of the item being stored in cache.</typeparam>
public class ServiceCacheItem<TValue>
{
/// <summary>
/// Creates a new instance of this type.
/// </summary>
public ServiceCacheItem()
{
this.TimeCaptured = DateTime.Now;
}

/// <summary>
/// Gets or sets the key, or unique identifier for this item.
/// </summary>
public String Key { get; set; }

/// <summary>
/// Gets or sets the cached value.
/// </summary>
public TValue Value { get; set; }

/// <summary>
/// Gets the time this item was captured into cache.
/// </summary>
public DateTime TimeCaptured { get; protected set; }

/// <summary>
/// Returns a string that represents the current object.
/// </summary>
public override string ToString()
{
return String.Format("Key="{0}"; TimeCaptured="{1}"; Value="{2}"", Key, TimeCaptured, Value);
}
}

This is used by this abstract class called “ServiceCache”:

/// <summary>
/// Class to manage an in-memory cache of generic items.
/// </summary>
/// <typeparam name="TItem">The type of item that will be stored in cache.</typeparam>
public abstract class ServiceCache<TItem>
{
/// <summary>
/// Creates a new instance of this type. Defaults the cache lifetime to 10 minutes.
/// </summary>
protected ServiceCache()
{
this.CacheLifetime = new TimeSpan(0, 10, 0);
}

/// <summary>
/// Creates a new instance of this type. Defaults the cache lifetime to the specified
/// <paramref name="cacheLifetime"/>.
/// </summary>
/// <param name="cacheLifetime">The length of time for which items in the cache should
/// be considered valid. Afterwards, the items beyond this time span will be considered
/// stale and removed from cache.</param>
protected ServiceCache(TimeSpan cacheLifetime)
: this()
{
this.CacheLifetime = cacheLifetime;
}


/// <summary>
/// Gets or sets the length of time the items in the cache are considered good. Will take
/// affect the next time <see cref="GetResult"/> is called, which cleans up the cache.
/// </summary>
public TimeSpan CacheLifetime { get; set; }

/// <summary>
/// Gets an enumeration of the currect things in cache.
/// </summary>
public abstract IEnumerable<ServiceCacheItem<TItem>> CachedItems { get; }


/// <summary>
/// Gets an item from the cache. If it is not there, will execute <see cref="CacheMiss"/>
/// delegate to retrieve the item, store it in cache - and return it.
/// </summary>
/// <param name="key">The key, or unique identifier of the item in cache.</param>
/// <returns>A populated object, either from cache - or from the underlying method specified
/// by the <see cref="CacheMiss"/> delegate.</returns>
/// <exception cref="ArgumentException">When <paramref name="key"/> is null or empty.</exception>
public TItem GetResult(String key)
{
if (String.IsNullOrEmpty(key))
throw new ArgumentException("Argument "key" cannot be null or empty.", "key");

PruneCache();

ServiceCacheItem<TItem> cachedItem =
CachedItems.Where(item => item.Key.Equals(key, StringComparison.CurrentCultureIgnoreCase)).FirstOrDefault();

return ProcessCacheSearchOutcome(key, cachedItem);
}

/// <summary>
/// Prunes away all stale items in cache. Automatically called with every call to <see cref="GetResult"/>.
/// </summary>
public virtual void PruneCache()
{
IEnumerable<ServiceCacheItem<TItem>> expiredItems = CachedItems.Where(item =>
DateTime.Now.Subtract(item.TimeCaptured).TotalMinutes > CacheLifetime.TotalMinutes);

ServiceCacheItem<TItem>[] expiredItemsArray = expiredItems.ToArray();

for (int index = 0; index < expiredItemsArray.Length; index++)
{
Debug.WriteLine("Removing item from cache... " + expiredItemsArray[index].Key);
RemoveFromCache(expiredItemsArray[index]);
}
}

/// <summary>
/// Gets or sets the function pointer to use, if the item key is not found in the cache.
/// </summary>
public Func<String, ServiceCacheItem<TItem>> CacheMiss { get; set; }


/// <summary>
/// Abstract: In a derived class, adds an item to the cache store.
/// </summary>
/// <param name="item">The item to add to the store.</param>
public abstract void AddToCache(ServiceCacheItem<TItem> item);

/// <summary>
/// Abstract: In a derived class, removes an item from the cache store.
/// </summary>
/// <param name="item">The item to remove from the store.</param>
public abstract void RemoveFromCache(ServiceCacheItem<TItem> item);

/// <summary>
/// Abstract: In a derived class, removes all items from the cache.
/// </summary>
public abstract void ClearCache();

#region Private Support Members

/// <summary>
/// Establish whether the <paramref name="cachedItem"/> we got from cache is valid or not.
/// </summary>
/// <exception cref="ArgumentException"/>
private TItem ProcessCacheSearchOutcome(String key, ServiceCacheItem<TItem> cachedItem)
{
if (String.IsNullOrEmpty(key))
throw new ArgumentException("Argument "key" cannot be null or empty.", "key");

if (cachedItem == null)
{
Debug.WriteLine("Item "" + key + "" NOT found in cache. Cache age is set to: " + CacheLifetime);
ServiceCacheItem<TItem> newItem = CacheMiss(key);
AddToCache(newItem);

return newItem.Value;
}
else
{
Debug.WriteLine("Item "" + key + "" found in cache - using it. " +
DateTime.Now.Subtract(cachedItem.TimeCaptured) + " minutes old.");
return cachedItem.Value;
}
}

#endregion
}

I ended up here because I realized that I may want to persist this cache a couple of different ways. Even better, I realized that I need only expose a way to: Add, Remove, Clear, and an IEnumerable<T> and that was technically all I needed. So, from this abstract class, I created a concrete implementation for in-process, or InProc storage of this cache (in the memory of the server):

/// <summary>
/// Service cache class that stores items in-memory.
/// </summary>
/// <typeparam name="TItem">The data type of the item to store in cache.</typeparam>
public class InProcServiceCache<TItem> : ServiceCache<TItem>
{
/// <summary>
/// Creates a new instance of this type.
/// </summary>
public InProcServiceCache()
: base()
{
}

/// <summary>
/// Creates a new instance of this type, specifying a default cache lifetime.
/// </summary>
/// <param name="cacheLifetime">The length of time in which cache items are considered valid.</param>
public InProcServiceCache(TimeSpan cacheLifetime)
: base(cacheLifetime)
{
}

/// <summary>
/// Gets an enumeration of the currect things in cache.
/// </summary>
public override IEnumerable<ServiceCacheItem<TItem>> CachedItems
{
get { return cachedItems; }
} protected List<ServiceCacheItem<TItem>> cachedItems = new List<ServiceCacheItem<TItem>>();

/// <summary>
/// Adds an item to the cache.
/// </summary>
/// <param name="item">The item to add to the cache.</param>
/// <exception cref="ArgumentNullException">When <paramref name="item"/> is null.</exception>
public override void AddToCache(ServiceCacheItem<TItem> item)
{
if (item == null)
throw new ArgumentNullException("item", "Argument "item" cannot be null.");

cachedItems.Add(item);
}

/// <summary>
/// Removes an item from the cache.
/// </summary>
/// <param name="item">The item to remove from the cache.</param>
/// <exception cref="ArgumentNullException">When <paramref name="item"/> is null.</exception>
public override void RemoveFromCache(ServiceCacheItem<TItem> item)
{
if (item == null)
throw new ArgumentNullException("item", "Argument "item" cannot be null.");

cachedItems.Remove(item);
}

/// <summary>
/// Clears the cache of all items.
/// </summary>
public override void ClearCache()
{
cachedItems.Clear();
}
}

All I needed to do was provide the specific functionality to store the items in a List<T>. I could presumably write a SqlServiceCache, XmlFileServiceCache, or whatever else comes to mind. There are some (somewhat infuriating) people that would shout out “YAGNI!” (or “you aint’ gonna need it” – the idea that you shouldn’t write code that you need right now.

I took about 7 minutes to go from a proprietary/concrete implementation that was brittle/fragile and not extensible – to  this abstract class. That, friends, is “cheap insurance”. In fact, it doesn’t get much cheaper. Imagine instead I took that famous YAGNI approach and in a year, I needed to change how this cache is persisted. At that point, we have an entire system built on top of the brittle way – that would be major surgery! So although true, I currently don’t… aint’t… need it right now, it doesn’t mean that it wasn’t worth the 7 minutes.

So, is YAGNI valid, then? Of course! Don’t over-engineer things for the sake of over-engineering them – that’s the spirit of the message. HOWEVER, A) lazy duh-velopers use YAGNI as an excuse to write crappy, brittle code and B) taking the 5 minutes to simply choose a loosely-coupled direction is NOT YAGNI – it is a good design decision. Don’t use valid, good principles to prop up your half-baked excuses on why you can be a lazy coder!! I’m just sayin’, is all… 🙂

Putting it all together:
OK, so at this point – this is a working system. All the code I’m using for this is attached in this e-mail. I only store the cache lifetime and the API key in the config file. Aside from that, this is all there is to it.

<add key="wundergroundApiKey" value="01234567890123456789"/>
<add key="wundergroundCacheDuration" value="00:10:00"/>

Also, to be sure this was acting the way I thought it should, I put in some debug statements and sure enough, as I refresh the web page:

Item "90731" NOT found in cache. Cache age is set to: 00:10:00
Item "01108" NOT found in cache. Cache age is set to: 00:10:00
Item "34667" NOT found in cache. Cache age is set to: 00:10:00
Item "06084" NOT found in cache. Cache age is set to: 00:10:00
Item "34607" NOT found in cache. Cache age is set to: 00:10:00

Item "06084" found in cache - using it. 00:00:03.7769986 minutes old.
Item "34667" found in cache - using it. 00:00:03.8270026 minutes old.
Item "34607" found in cache - using it. 00:00:03.8360060 minutes old.
Item "90731" found in cache - using it. 00:00:03.8430074 minutes old.
Item "01108" found in cache - using it. 00:00:03.8520088 minutes old.

Item "34607" found in cache - using it. 00:00:09.4595084 minutes old.
Item "90731" found in cache - using it. 00:00:09.4485069 minutes old.
Item "06084" found in cache - using it. 00:00:09.4035018 minutes old.
Item "01108" found in cache - using it. 00:00:09.4555083 minutes old.
Item "34667" found in cache - using it. 00:00:09.5045116 minutes old.

So we have some JQuery AJAX calls that are run from an existing MVC view. That talks to a regular MVC controller action. That goes to our service provider to go get the JSON. That service provider checks the ServiceCache – which automagically either gets the item from cache, or calls your delegate to go get the data from the actual service.

Ultimately that JSON is returned to the client and UI is rendered. I was reading that there are other ways around this with “JSONP”, but this approach allows you to do server-side caching, plus it hides your API key from anyone who looks at the source. Both of those are important reasons, so I tend to lean more towards this approach anyhow.

Also, that ServiceCache<T> class can now be used to cache ANYthing, and in particular, any results from any web service. Since it’s generic, you can specify whatever type you’d like!

Posted in ASP.NET, ASP.NET MVC, JQuery, Uncategorized, WCF, Web Services

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Archives
Categories

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 2 other followers

%d bloggers like this: