There's a blob on your web page - but don't wipe it off [New Delay.Web.Helpers assembly brings easy Amazon S3 blob access to ASP.NET web sites!]
Microsoft released a bunch of new functionality for its web platform earlier today - and all of them are free! The goodies include new tools like WebMatrix, ASP.NET MVC3 and the Razor syntax, and IIS Express - as well updates to existing tools like the Web Platform Installer and Web Deploy. These updates work together to enable a variety of new scenarios for the web development community while simultaneously making development simpler and more powerful. I encourage everyone to check it out!
Web Helpers...
One of the things I find particularly appealing is the idea of "web helpers": chunks of code that can be easily added to a project to make common tasks easier - or enable new ones! Helpers can do all kinds of things, including integrating with bit.ly, Facebook, PayPal, Twitter, and so-on. For an example of cool they are, here's how easy it is to add an interactive Twitter profile section to any ASP.NET web page [and it's even easier if you don't customize the colors :) ]:
@Twitter.Profile( "DavidAns", height:150, numberOfTweets:20, scrollBar:true, backgroundShellColor:"#8ec1da", tweetsBackgroundColor:"#ffffff", tweetsColor:"#444444", tweetsLinksColor:"#1985b5")
Which renders as:
You can see other examples of useful web helpers (including charting, cryptography, Xbox gamer cards, ReCaptcha, and more) in the ASP.NET social networking tutorial.
Aside: While web helper assemblies like System.Web.Helpers and Microsoft.Web.Helpers are obviously easy to use from ASP.NET Razor and ASP.NET MVC, what's neat (and not immediately obvious) is that they don't need to be tied to those technologies. At the end of the day, helper assemblies are normal .NET assemblies and some of their functionality can be used equally well in non-web scenarios!
The inspiration...
One of the web helpers that caught my eye when I was looking around is the Windows Azure Storage Helper which makes it easy to incorporate Windows Azure blob and table access into ASP.NET applications. If you happened to be reading this blog a few months ago, you might remember that I already have some experience working with Windows Azure and Amazon S3 blobs in the form of my BlobStore Silverlight application and BlobStoreOnWindowsPhone Windows Phone 7 sample.
When I wrote BlobStore
for Silverlight, I didn't see any pre-existing libraries I could make use of (the Windows Azure Managed Library didn't (and maybe still doesn't?) work on Silverlight and there were some licensing concerns with the S3 libraries I came across), so I implemented my own support for what I needed: list/get/set/delete support for blobs on Windows Azure and Amazon Simple Storage Service (S3). But now that we're talking about ASP.NET, web helpers run on the server and the Windows Azure Storage Helper is able to leverage the Windows Azure Managed Library for all the heavy lifting so it can expose things in a clean, web helper-suitable way (via the WindowsAzureStorage
static class and its associated methods).
That's good stuff and the WindowsAzureStorage
helper seems quite comprehensive - but what about customers who are already using Amazon S3 and can't (or won't) switch to Windows Azure? Well, that's where I come in. :) I'd thought it would be neat to create my own web helper assembly, and this seemed like the perfect opportunity - so I've created the Delay.Web.Helpers
web helper assembly by leveraging the work I'd already done for BlobStore
. This enables basic list/get/set/delete functionality for S3 blobs without a much additional effort and seems like a great way to help out more customers!
The API...
Most people don't like having many different ways to do the same thing, so I decided early on that the fundamental API for AmazonS3Storage
would be the same as that already exposed by WindowsAzureStorage
. That led directly to the following API (the two "secret key" properties have been renamed to match Amazon's terminology, but they're used exactly the same):
namespace Delay.Web.Helpers { public static class AmazonS3Storage { public static string AccessKeyId { get; set; } public static string SecretAccessKey { get; set; } public static IList<string> ListBlobs(string bucketName); public static void UploadBinaryToBlob(string blobAddress, byte[] content); public static void UploadBinaryToBlob(string blobAddress, Stream stream); public static void UploadBinaryToBlob(string blobAddress, string fileName); public static void UploadTextToBlob(string blobAddress, string content); public static byte[] DownloadBlobAsByteArray(string blobAddress); public static void DownloadBlobAsStream(string blobAddress, Stream stream); public static void DownloadBlobToFile(string blobAddress, string fileName); public static string DownloadBlobAsText(string blobAddress); public static void DeleteBlob(string blobAddress); } }
And because I wanted to be sure AmazonS3Storage
could really be dropped into an application that already used WindowsAzureStorage
, I did exactly that: I downloaded the WindowsAzureStorage
sample application, performed a find-and-replace on the class name, updated the names of the two "secret key" properties, removed some unsupported functionality, and then verified that the sample ran and worked correctly. (It did!)
The enhancements...
While I think the WindowsAzureStorage
APIs works well for listing and reading existing blobs, it's not immediately clear what a blobAddress
string is made up of or how to create one, so I might get a little confused in scenarios that create blobs from scratch... It so happens that the blobAddress
structure is ContainerName/BlobName
(which I preserve in AmazonS3Storage
), but that feels like an implementation detail developers focusing on the upload scenario shouldn't need to know. Therefore, I've also exposed the following set of methods with separate bucketName
and blobName
parameters (and otherwise the same signatures):
public static void UploadBinaryToBlob(string bucketName, string blobName, byte[] content); public static void UploadBinaryToBlob(string bucketName, string blobName, Stream stream); public static void UploadBinaryToBlob(string bucketName, string blobName, string fileName); public static void UploadTextToBlob(string bucketName, string blobName, string content); public static byte[] DownloadBlobAsByteArray(string bucketName, string blobName); public static void DownloadBlobAsStream(string bucketName, string blobName, Stream stream); public static void DownloadBlobToFile(string bucketName, string blobName, string fileName); public static string DownloadBlobAsText(string bucketName, string blobName); public static void DeleteBlob(string bucketName, string blobName);
I'm optimistic this provides a somewhat easier model for developers to work with, but I encourage folks to use whatever they think is best! :)
Aside: I hadn't previously needed create/list/delete functionality for buckets, so the corresponding methods are not part of today's release. However, I expect them to be rather easy to add and will likely do so at a future date.
The testing...
To try to ensure I hadn't made any really dumb mistakes, I wrote a complete automated test suite for the new AmazonS3Storage
class. With the exception of verifying each ArgumentNullException for null
parameters, the test suite achieves complete coverage of the new code and runs as a standard Visual Studio test project (i.e., here's an example of using web helpers "off the web").
The sample...
As much fun as testing is, a sample is often the best way to demonstrate how an API is used in practice - so I also put together a (very) quick Razor-based web site to provide simple list/get/set/delete access to an arbitrary Amazon S3 account/bucket:
The example...
Using the Delay.Web.Helpers
web helper is as easy as you'd expect; here's the piece of code that lists the bucket's contents (which is only as long as it is because it handles invalid credentials and empty buckets):
@if (configured) { <p class="Emphasized"> Contents of bucket <strong>@bucket</strong>: </p> var blobs = AmazonS3Storage.ListBlobs(@bucket); if (blobs.Any()) { foreach (var blobAddress in blobs) { <p> <a href="@Href("DownloadBlob", new { BlobAddress = blobAddress})">@blobAddress.Split('/').Last()</a> - <a href="@Href("DeleteBlob", new { BlobAddress = blobAddress})" class="Subdued">[Delete]</a> </p> } } else { <em>[Empty]</em> } } else { <p> <strong>[Please provide valid Amazon S3 account credentials in order to use this sample.]</strong> </p> }
The one "gotcha" that's easy to forget (and that causes compile errors when you do!) is the using statement that goes at the top of the .cshtml
file and tells Razor where to find the implementation of the AmazonS3Storage
class:
@using Delay.Web.Helpers
The download...
To use the Delay.Web.Helpers
assembly in your own projects, simply download the ZIP file above, unblock it (click here for directions on "unblocking" a file), extract its contents to disk, and reference Delay.Web.Helpers.dll
- or copy the Delay.Web.Helpers.dll
and Delay.Web.Helpers.xml
files to wherever they're needed (typically the Bin
directory of a web site).
Aside: Technically, the Delay.Web.Helpers.xml
file is optional - its sole purpose is to provide enhanced IntelliSense (specifically: method and parameter descriptions) in tools like Visual Studio. If you don't care about that, feel free to omit this file!
To play around with the included sample web site, just open the folder Delay.Web.Helpers\SourceCode\SampleWebSite
in WebMatrix or Visual Studio (as a web site) and run it.
To have a look at the source code for the Delay.Web.Helpers
assembly, tweak it, run the automated tests, or open the sample web site (a slightly different way), just open Delay.Web.Helpers\SourceCode\Delay.Web.Helpers.sln
in Visual Studio.
The summary...
Being able to repurpose code is a great thing! In this case, all it took to convert my existing BlobStoreApi
into an ASP.NET Razor/MVC-friendly web helper was writing a few thin wrapper methods. With a foundation like that, all it takes is some testing to catch mistakes, a quick sample to show it all off, and all of a sudden you've got yourself the makings of a snazzy blog post - and a new web helper! :)