Using The Amazon Web Service From ASP.NET

Tuesday, June 22, 2004

In this article we will be utilizing the Amazon Web Service (AWS) from an ASP.NET web form. Instead of taking a comprehensive look at the entire API we will concentrate on a single method to retrieve product information. In developing the sample we will take a look at abstraction techniques to make the web service easier to use in a web form scenario.

 

Our goal in this article is to build a user control which we can place on a web form. The user control will retrieve information about a book and display the information, such as price and availability, with a minimum of effort from the form developer. The ideal usage would look similar to the following:

<%@ Register TagPrefix="uc1" TagName="Amazon_ShowBook" Src="Amazon_ShowBook.ascx" %>
.
.
.
<uc1:Amazon_ShowBook id="Amazon_ShowBook1" runat="server" ASIN="1904811000"/>

ASIN is short for “Amazon Standard Item Number”. For books, the ASIN is the same as the ISBN. Every product on amazon.com has as ASIN which you can find on the product’s page. The end result should look like the following screen shot:

Setting Up References

For starters, you’ll need to download the Amazon.com Web Services Developer’s Kit. The kit is free, although you must apply for your own, unique developer token to use in the web service calls, as we will see later. It is important for you to read the terms and conditions before beginning development, as amazon.com has specific rules on how you use the API and display the information, including how frequently you can make calls to the API.

Once the kit and developer token are in hand, you are ready to begin a new web project. Right click the project and select “Add Web Reference”. In the dialog box that follows, we will go to the URL for the WSDL describing AWS at http://soap.amazon.com/schemas3/AmazonWebServices.wsdl. Before clicking the Add Reference button you might want to give the web reference a different name (in the lower right textbox), in this example we will use AmazonWS. After clicking the Add Reference button you’ll have a proxy class in your project for the web service. Calling methods on the proxy class will result in a network hop across the Internet to amazon’s servers, where the information you request will be located and sent back to your application. Let’s take a look at a simple example.

Search Request, First Attempt

The following code snippet demonstrated a quick and dirty call to Amazon. We will discuss some of the basics about this call before moving on to improve the code.

AmazonWS.AsinRequest asinRequest = new AmazonWS.AsinRequest();
asinRequest.asin = "1904811000";
asinRequest.devtag = "1234567890AAA";
asinRequest.type = "heavy";
asinRequest.tag = "webservices-20";

AmazonWS.AmazonSearchService amazonSearch = new AmazonWS.AmazonSearchService();
AmazonWS.ProductInfo awsInfo;
awsInfo = amazonSearch.AsinSearchRequest(asinRequest);

priceLabel.Text = awsInfo.Details[0].OurPrice;

Before we call the web service we need to setup an object with the information the server needs to fulfill our request. The AsinRequest class has a number of fields to populate when searching for a specific ASIN. In the above code we are populating the four required fields. The asin in this example is the ISBN for a book. The devtag is the developer token given to us by Amazon. The type field can be set to “lite” or “heavy”, depending on the amount of detail you want the server to return. Finally, the tag field holds your Amazon associate’s identifier. If you are not an Amazon associate you can pass a generic associate ID of “webservices-20”.

With the AsinRequest object populated we can make a call to the service using the AmazonSearchService class. When successful the call will give us back a ProductInfo object, which contains quite a bit of information about the book. In the example above we are pulling out the price for the book.

There are just a couple problems with the code from an architectural viewpoint. The values are hardcoded, and the developer needs to know a good bit of detail to dig out the required information from the ProductInfo object which we can simplify. Now that we understand better how this all works, let’s try to improve.

Search Request : Second Attempt

Our first step will be to write our own class to hold product information. By doing so anyone who is going to use our code won’t need to learn the Amazon classes, but instead can use a simpler class tailored for our application needs.

public class ProductInfo
{
   public string Title = "unknown";
   public string Url = String.Empty;
   public string Price = "0.00";
   public string Availability = "Unavailable";
   public string Reviews = "0";
   public string Authors = String.Empty;
}

Not a particularly interesting class, but it will be easy to use and mildly abstracts away the Amazon structure from the rest of the application. Next, we will write a service helper class. The service helper class will do all the hard work of calling the web service and organizing the results into our own ProductInfo object. The first method we write is shown below.

private static ProductInfo GetProductInfoFromAWS(string asin)
{
   AmazonWS.AsinRequest asinRequest = new AmazonWS.AsinRequest();
   asinRequest.asin = asin;
   asinRequest.devtag = AWSConfig.DEVTAG;
   asinRequest.type = AWSConfig.TYPE_HEAVY;
   asinRequest.tag = AWSConfig.TAG;

   AmazonWS.AmazonSearchService amazonSearch = new AmazonWS.AmazonSearchService();
   AmazonWS.ProductInfo awsInfo;
   awsInfo = amazonSearch.AsinSearchRequest(asinRequest);

   ProductInfo info = new ProductInfo();
   
   if(awsInfo != null)
   {
      if(awsInfo.Details.Length > 0 && awsInfo.Details[0] != null)
      {
         if(awsInfo.Details[0].ProductName != null)
            info.Title = awsInfo.Details[0].ProductName;
         
         if(awsInfo.Details[0].Authors.Length > 0)
            info.Authors = string.Join(", ", awsInfo.Details[0].Authors);
         
         if(awsInfo.Details[0].Availability != null)
            info.Availability = awsInfo.Details[0].Availability;

         if(awsInfo.Details[0].OurPrice != null)
            info.Price = awsInfo.Details[0].OurPrice;
         
         if(awsInfo.Details[0].Reviews != null && 
            awsInfo.Details[0].Reviews.TotalCustomerReviews != null)
               info.Reviews = awsInfo.Details[0].Reviews.TotalCustomerReviews;
         
         if(awsInfo.Details[0].Url != null)
            info.Url = awsInfo.Details[0].Url;
      }
   }

   return info;
}

Notice we have pushed all of the values we hard-coded in the first step (devtag, type) into another class with the name of AWSConfig. AWSConfig can pull these items from the web.config file using the ConfigurationSettings class of .NET, or from a database or another XML file. We now have slightly more flexible and robust code.

After the web service call, the method carefully extracts the information we need from the Amazon ProductInfo object and populates our own ProductInfo object. The Amazon web service will return null values for data items which do not exist, so we need to carefully check for null before overwriting the default values in our ProductInfo object.

Notice we declared the method as private. The application code will not call this method directly. Instead, we are going to add some additional methods to enable caching of the information.

public static ProductInfo GetProductInfo(string asin)
{
   ProductInfo info = null;

   info = GetProductInfoFromCache(asin);

   if(info == null)
   {
      info = GetProductInfoFromAWS(asin);
      CacheProductInfo(asin, info);
   }

   return info;         
}

static ProductInfo GetProductInfoFromCache(string asin)
{
   string key = FormatCacheKey(asin);
   
   ProductInfo info = null;
   info = HttpRuntime.Cache[key] as ProductInfo;
   return info;            
}

static void CacheProductInfo(string asin, ProductInfo info)
{
   string key = FormatCacheKey(asin);
   HttpRuntime.Cache.Insert(
         key, 
         info, 
         null, 
         DateTime.Now.AddHours(12), 
         TimeSpan.Zero
      );
}

static string FormatCacheKey(string asin)
{
   return "AWS_PRODUCT" + asin;
}      

Now we will cache the results for 12 hours and avoid the overhead of the web service call. (Reminder: make sure to read the terms and conditions documents for AWS closely as Amazon has stipulations on how long you can cache specific pieces of information). The public method GetProductInfo will first check to see if there is a result in the cache before calling GetProductInfoFromAWS. Once it has a result it will cache the result using a key that includes the ASIN value to ensure the key is unique.

Assuming we designed our user control with simple Label and Hyperlink Webform controls, the Page_Load event handler for the control should look relatively simple. We just need to give the service helper class an ASIN and wait for the results to return.

private void Page_Load(object sender, System.EventArgs e)
{
   if(!IsPostBack)
   {
      Amazon.ProductInfo info;         
      info = Amazon.ServiceHelper.GetProductInfo(asin);
      
      titleLink.NavigateUrl = info.Url;
      titleLink.Text = info.Title;
      authorLabel.Text = info.Authors;
      priceLabel.Text = info.Price;
      availabilityLabel.Text = info.Availability;
      reviewLabel.Text = info.Reviews;
   }
}
public string ASIN
{
   get { return asin; }
   set { asin = value; }
}

string asin = null;

Notice we have also given the user control a public property ASIN. This allows us to specify the ASIN to lookup inside of the control tag in the ASPX, as we demonstrated at the beginning of the article. We could also programmatically assign the ASIN as the result of a user request.

Now what is missing? We have made the Amazon product search easy to use for our web form developer. We have also made it clear through the naming convention (Amazon.ServiceHelper.GetProductInfo) that the code is making a web service call. The developer should realize this code could fail for any number of reason even though the parameters are all correctly initialized. Network connectivity could be lost, or the service could be temporarily unavailable.

Dealing with the increased risk of exceptions is an important aspect of using web services. We could try a simplistict error handler in our user control like the one shown below:

try 
{ 
  info = Amazon.ServiceHelper.GetProductInfo(asin); 
} 
catch(Exception) 
{ 
  this.Visible = false; 
} 

This technique is the “head in the sand approach”. We simply don’t display the user control at all if an exception occurs. If this is acceptable behavior for your application than the world is good, but think of what else is happening behind the scenes. For every user who hits the page with this user control the application will attempt a web service call and must wait to timeout if the network is down. All this waiting for a large number of users can spell disaster for performance. Let’s try to make a few more changes to improve the situation.

Search Request : Third Attempt

Our third attempt adds some additional code to the GetProductInfo method. The last iteration is shown below.

public static ProductInfo GetProductInfo(string asin)
{
   ProductInfo info = null;        

   info = GetProductInfoFromCache(asin);

   if(info == null)
   {
      try
      {
         info = GetProductInfoFromAWS(asin);
      }
      catch(Exception)
      {
         // log the error...
      }

      
      if(info == null)
      {
         info = new ProductInfo();
         CacheProductInfo(
               asin, info, DateTime.Now + AWSConfig.CACHETIME_ON_ERROR
            );
         
      }
      else
      {
         CacheProductInfo(
               asin, info, DateTime.Now + AWSConfig.CACHETIME_ON_SUCCESS
            );
      }
   }

   return info;         
}

static void CacheProductInfo(string asin, ProductInfo info, DateTime expiration)
{
   string key = FormatCacheKey(asin);
   HttpRuntime.Cache.Insert(
         key, 
         info, 
         null,
         expiration, 
         TimeSpan.Zero
      );
}

Notice we have added a try catch block now. What you do when an exception occurs will depend on the needs of your application. Perhaps you’ll need to log the failure to a database, or to the event log, or send an email to an administrator.

We’ve also added two new configuration items to configure the cache. When an error occurs we will create a new ProductInfo object and cache the object for a short amount of time (say, 5 minutes). Hopefully, in 5 minutes the service will be available again, and we won’t be blocking all of the users waiting for the service response during this time. If the call succeeded we can cache the data for a longer period of time.

Summary

In this article we’ve designed and built an interface to the Amazon Web Service. Hopefully you’ll find this article helpful as an introduction to AWS as well as pick up some design guidelines to use in your own web service client application. In future articles we can addresses threading issues and how to keep concurrent request beneath an agreed upon minimum.

-- Scott Allen.

by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!