Serving Images from .NET Assemblies

 

Morgan Skinner
Microsoft Developer Services

June 2003

Summary: Most if not all Web sites require images for their user interface, and these are usually stored on disk. This article shows how images can be served from an assembly, which can avoid the proliferation of numerous files on disk, can simplify installation and configuration of the Web server, and can increase the security of those images. (15 printed pages)

Applies To:
   Microsoft® .NET Framework versions 1.0 and 1.1
   Microsoft® Visual C#®
   Microsoft® ASP.NET

Download the MFRImages.exe sample file. The download includes readme.htm, which describes how to configure the example.

Contents

Introduction
Overview of the Problem
Serving an Image
Serving an Image from an ASPX Page
Serving an Image from a Custom Handler
Hardening the Code
Tidying Up
Security
Conclusion
About the Author

Introduction

One of the most often-heard grumbles from programmers who have written controls for Windows and then move over to the Web is, "Why can't I store images within the same assembly as the control?" The simple answer is you can, you just need to know how to. This article shows how images can be served from within an assembly, and provides two alternate methods of retrieving images. Download the MFRImages.exe at the top of the page to explore more completely the example code I discuss below.

Overview of the Problem

An image on a Web site is always referred to by a URL, for example http://www.morganskinner.com/images/morganskinner.gif. This tells the Web client where to look for the image, which is downloaded separately from the text in the Web page. Images are typically stored on a Web server in a subdirectory with a name such as /images, and pages simply provide references to these images so that they can be displayed on the client browser.

When you create a custom control the same holds true—images generally need to be persisted on disk before they can be downloaded to the user. As a control writer you would like to provide a single assembly that contains not only your control, but also default images as appropriate. It's advantageous for a control to be self contained, as this requires less configuration by the user, who may forget to copy a number of image files onto a production server, resulting in a less than pleasing result for the user.

Another possible reason for wanting to bind resources into assemblies is to ensure that users cannot alter those resources—an example would be corporate branding, when specific images should always be used rather than someone inadvertently saving the wrong image on disk with, for example, an incorrect typeface or color. If your assembly has been strong named, when loading it you can test that it has not been tampered with in any way.

The main problem with serving images from an assembly is that HTTP requires a URL for an image—you can't just return a sequence of bytes to the user inline with the HTML and expect that the image will be displayed correctly. Some way of redirecting the image request to a resource within an assembly is necessary, and I'll present two methods in this article.

Before we get going, however, there's one thing to note. It isn't possible to provide a totally self-contained control that will render images correctly without some other configuration being done on the Web server. You'll need to create at least one other file on the server, or perform some changes to the IIS metabase in order to deliver images to the client;, however, after these simple changes have been made once you can serve images from any assemblies with ease.

Serving an Image

As the main crux of the matter is to serve an image, let's get that bit over and done with straight away. We need a method that, when passed some information from the caller, will load an image and return it in the response stream. Note that in this example the only type of image being served is a .gif—to serve other types of image the content type and image format would need to be inferred from the image extension.

The following function shows how an image can be loaded from a given assembly, and returned on the HttpResponse stream to the client. We'll use this function as the basis of our code going forward, and add much needed functionality such as exception processing and image caching on the way. I'll define this function as a static within the ManifestImageLoader class.

public class ManifestImageLoader
{
public static void RenderImage ( string assembly , string image , HttpContext context ){      Assembly   resourceAssem = Assembly.Load ( assembly ) ;      // Get the resource      using ( Stream   imageStream = resourceAssem.GetManifestResourceStream ( image ) )      {         // And if that's OK, write it out         using ( System.Drawing.Image theImage =               System.Drawing.Image.FromStream ( imageStream ) )
            response.ContentType = "image/gif" ;
            theImage.Save ( context.Response.OutputStream , ImageFormat.Gif ) ;      }}}

The function, RenderImage, accepts an assembly name, image name, and the response stream. Given valid assembly and image names, it's trivial to load an image and return it to the output stream. First the assembly is loaded, and then we use the Assembly.GetManifestResourceStream function to return a stream over the named resource, which in this instance is an image. You'll need to add a using clause for System.Reflection and System.IO to get this to compile, and also reference the System.Drawing assembly.

Once we have the image stream, an image can be constructed from that stream of bytes using the Image.FromStream ( ) method—note that we're using the image class within System.Drawing rather than the similarly named class in System.Web.UI.WebControls, as the former wraps access to Win32 image functions, whereas the latter wraps an <img> tag with a Web control.

If you're not familiar with the C# using syntax, it wraps the code in a try/finally block, and ensures that Dispose is called on the item within the parentheses.

Now we can serve an image from an assembly, we need to be able to create a URL for that image in the first place. The first method is by using an .ASPX page.

Serving an Image from an ASPX Page

This first method of serving up an image requires an .ASPX page, which naturally will have to reside somewhere on the server. The page itself has no content—its primary function is to retrieve parameters from the URL and use these to retrieve the image.

As an example, let's say we have a page called ImageFromASPX.aspx that resides at the root of the Web site. You can then define all of your images to be served from this page, using the following URL encoding syntax within HTML. Here we're serving up a banner image called winxp.gif.

<img src="/imagefromASPX.aspx?assem=ImageServer&amp;image=winxp.gif" />

Within the URL we have defined the path to the ASPX page, and then defined two parameters, one being the name of the assembly (in this instance ImageServer), the other being the name of the image. This may be a security risk, so later in the article I'll present an encryption method for this data.

Within the code for the ASPX page we can then write the following:

private void Page_Load ( object sender, System.EventArgs e )
{
   // Retrieve the parameters
   string assembly = Request.QueryString["assem"] ;
   string image = Request.QueryString["image"] ;

   // And load the image
   ManifestImageLoader.RenderImage ( assembly , image ) ;
}

All the code does is parse the parameters on the request, and then call the RenderImage function that we wrote earlier. There isn't much to this implementation, as you can see; however, there is one drawback in that all requests for images need to go through the same URL—that is, each custom control needs to know the location and the name of the imageserver.aspx file in order to serve images. Avoiding that limitation is the subject of the next section.

Serving an Image from a Custom Handler

If you've never come across the subject of a handler before, I'll provide a quick overview. A handler is an object that implements the IHttpHandler interface, and is called for a particular verb (POST, GET, etc.) or set of verbs when a request with a given file extension is passed through the ASP.NET pipeline.

In basic terms, ASP.NET checks the file extension of the request, and routes the request to the handler that has been associated with that extension.

Armed with this knowledge, we can create a handler, associate it with our own file extension (so that ASP.NET knows we want to call our handler, not something else), and serve images up that way.

The code below shows a simple handler that uses the RenderImage function declared above.

public class ManifestResourceHandler : IHttpHandler
{
   /// <summary>
   /// Process the request for the image
   /// </summary>
   /// <param name="context">The current HTTP context</param>
   void IHttpHandler.ProcessRequest ( System.Web.HttpContext context )
   {
      // Get assembly name and resource name from the request
      string assembly = context.Request.QueryString["assem"] ;
      string image = context.Request.QueryString["image"] ;      // Then load the image and return to the caller      ManifestImageLoader.RenderImage ( assembly , image ) ;   }   /// <summary>   /// This handler can be reused, it doesn't need to be recycled   /// </summary>   bool IHttpHandler.IsReusable   {      get { return true; }   }}

The code is very similar to that shown for the ASPX page above—read the parameters from the passed URL then pass these through to the RenderImage function.

Now, to serve an image up using a handler we need to use a different URL. In this instance we need to create a made-up file extension (that is, one that doesn't already exist within IIS), so that requests for an image are routed to the correct handler. In this example I'll use "mfr" (for manifest resource). A request for an image now will look something like the following.

<img src=".mfr?assem=MS.Resources&amp;image=winxp.gif" />

Note that I've not specified a path to the resource, just the file extension .mfr.

The major benefit of using a handler is that it is called for all requests, regardless of their path.

Two other steps are necessary to get the handler to work. First you need to make a modification to your web.config to specify the new handler:

<configuration>
  <system.web>
    ...
    <httpHandlers>
      <add verb="GET" path="*.mfr" type="ImageServer.ManifestResourceHandler, ImageServer" />
    </httpHandlers>
  </system.web>
</configuration>

The type in the above config file defines the type and assembly where the handler is implemented. Note that the verb attribute is case sensitive, so it needs to be set to GET rather than any other capitalization style. The assembly itself needs to reside either within your Web site's bin directory, or can be installed in the Global Assembly Cache (GAC).

Next you need to edit the configuration of your Web server within IIS Admin. Click Properties for the Web site you would like to alter, choose the Home Directory tab, and click Configuration. This will display a window similar to that shown below.

Figure 1. Configuring IIS

Click the Add button to create an entry for the .mfr file type. Each extension is mapped to an ISAPI filter that processes the request for the resource. For ASP.NET this is the aspnet_isapi.dll filter. This library resides on disk beneath the version of the Framework you have installed, so to set up all requests for .mfr extensions to go through the appropriate ISAPI dll you need to set this as follows.

Version Path
1.0 C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\aspnet_isapi.dll
1.1 C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll

The rest of the settings are shown in the following image. Note that you must clear the Verify that file exists check box, otherwise the handler will never be called (as no file with .mfr physically exists on disk).

Figure 2. Setting properties for your extension

Your handler should now be operational. Type a URL in the browser that maps to a resource within your assembly:

Figure 3. Serving an image from an assembly

If instead of the image you requested you receive an exception, such as "'null' is not a valid value for 'stream'", you'll have hit one of the small problems that we haven't addressed in the code so far—what happens when there's an error in the image? We'll rectify this and other small niggles in the next section.

Hardening the Code

The first issue to address within the code is that of capitalization styles. HTTP considers all the following URLs to be identical, as URLs are not case sensitive.

<img src=".mfr?assem=ImageServer&amp;image=winxp.gif" />
<img src=".mfr?assem=ImageServer&amp;image=WINxp.gif" />
<img src=".mfr?assem=ImageServer&amp;image=WiNxP.gif" />

There is a problem with our code at present, as it doesn't preserve the case insensitive nature of the original HTTP request, so we need to add some more code into the LoadAndReturnImage function.

Case Insensitivity

The behavior we require is to load an image from an assembly without regard to case, however the Assembly.GetManifestResourceStream is case sensitive, so there has to be another way to do this. The Assembly.GetManifestResourceNames() function will return a list of all the resources within a given assembly, so all we need to do is call this, do a case-insensitive comparison with these resource names, and use any name found in the lookup:

Assembly   resourceAssem = Assembly.Load ( assembly ) ;

// Lookup the cached names
string[] names = HttpContext.Current.Application [ assembly ] as string[] ;

if ( null == names )
{
   // Get the names of all the resources in the assembly
   names = resourceAssem.GetManifestResourceNames() ;
   Array.Sort ( names , CaseInsensitiveComparer.Default ) ;
   HttpContext.Current.Application [ assembly ] = names ;
}

// If there are some resources in this assembly, 
// check for the one I want
if ( names.Length > 0 )
{
   // Find the image in the names array
   int pos = Array.BinarySearch ( names , image , 
CaseInsensitiveComparer.Default ) ;

   if ( pos > -1 )
      WriteImage ( resourceAssem , names[pos] , true ) ;
}

Here I load the assembly containing the resources, then lookup the application cache to see if the list of resources within that assembly has already been loaded and cached. If not, I read the list of resources by calling Assembly.GetManifestResourceNames(), sort this list, and store it in application state.

I can then perform a binary search on the list of names using the Array.BinarySearch() method. This is much faster than sequentially searching a list of strings, at the slight overhead of storing the list of resources in application state.

That takes care of case sensitivity, but what about performance? At present, each time a request for an image arrives we'll be calling all of this code, which for anything but the most trivial Web site is likely to cause major performance problems. We'll deal with this next.

Caching

Just like regular images and some ASPX pages, it would be advantageous to be able to cache the images returned from an assembly—since they reside in an assembly it is unlikely that they will change frequently, if ever.

If we were writing a simple ASPX page then we could add an OutputCache directive to have the page cached; however, in our scenario we need a method of programmatically adding the cache control headers to the response stream. Luckily, this too is very simple to accomplish in ASP.NET. In the function where we write the image to the output stream, we simply need to add the following lines:

response.Cache.SetExpires ( DateTime.Now.AddMinutes ( 60 ) ) ;
response.Cache.SetCacheability ( HttpCacheability.Public ) ;
response.Cache.VaryByParams["assem"] = true ;
response.Cache.VaryByParams["image"] = true ;
// Write the image to the response stream...

This sets the image to expire in an hour (this time can obviously be lengthened to lessen the server load), and defines that the image can be cached anywhere (client, proxy server, server, and so on). It also defines the parameters that alter the caching behavior.

The code is now nearly complete, but we need to decide what to do in the face of exceptions.

Programming for Exceptions

There are a number of exceptions that may be raised in our code, and at present the user will be rewarded with a broken link in their browser, or worse still an ASP.NET error page. There are a number of scenarios we can reasonably expect to occur. These are as follows.

  • The assembly may not exist.
  • The assembly may exist but not contain any images.
  • The assembly may not contain the requested image.

There may also be other errors thrown by the code. When an image is not found, the default browser response is to return an image with a red cross on it to indicate a broken link.

You may, quite rightly, want to replace this with a default image of your own. I've included a default broken link image in the ImageServer assembly, which will be returned to the browser in the face of exceptions. This behavior is configurable by adding a setting into the AppConfig section of the web.config file.

To override the default behavior (returning the exception up the chain) when an error occurs, add the following into the web.config.

<appSettings>
   <add key="MFRShowBrokenLink" value="true" />
</appSettings>

Now when an exception occurs in the code, the broken link image is returned to the browser and a warning is written into the trace log.

Figure 4. The image returned for a broken link

If you look at the trace log you'll see an entry something like the following for an image that does not exist.

Figure 5. Example Trace log output for an invalid image request

All the code discussed in this article is available from the MFRImages.exe download link at the top of this page. The download includes all of the hardening we've done in this section. It also includes some test pages where you can see the results of using the handler and the ASPX methods of rendering images.

Tidying Up

The next thing to add is a method that can return the properly formed URL for an image that resides within an assembly, which can then be called by a custom control writer (or you!) to return the image.

If you've chosen the handler method of exposing an image, all you need is a function such as the following.

public static string ConstructImageURL ( Assembly assembly, string image )
{
   return string.Concat ( ".mfr?assem=" , 
                     HttpUtility.UrlEncode ( assembly.FullName.ToString ( ) ) , 
                     "&image=" ,
                     HttpUtility.UrlEncode ( image ) ) ;
}

For this code, I'm using string.Concat() because it's approximately 4 times faster than string.Format(). Every little bit helps! You can then use this to set the ImageURL property of any images you create within your custom control.

Security

In our discussion so far, we've been serving images based on an assembly name and resource name. That isn't a bad thing, but it does mean that anyone can discover the name of an assembly on your disk, and can attempt an attack based on passing the name of any assembly to the handler.

To avoid this potential problem it would be wise to return a value that has been encrypted in some way. We could either serve some hashcode, generated from the name of the assembly plus the image, or use an encrypted form of the assembly name and image name, which is then decrypted when a request is received.

The former method (using a hashcode) requires a lookup table on the server, which is populated for each image served. This poses a potential problem on a Web farm where one server may serve the initial image request (and cache the hashcode), whereas another server may actually respond with the image itself.

For that reason I've chosen the second method, where the URL returned to the user contains the encrypted assembly name and image name. This doesn't suffer from the Web farm problem;, however, it does mean that slightly more data is required to be passed from the browser to the server as the image URL will be more lengthy.

The example code contains a class that encrypts and decrypts a string using the triple DES (data encryption standard) algorithm. In basic terms, the assembly name and image name are encrypted before being passed to the client. When the image is requested these values are decrypted and the same code as before is called.

I've added this in a configurable way to the solution. There's simply a flag in the web.config that, if set to "true", will encrypt the resource names as they are served to the client:

   <appSettings>
      <add key="MFRSecure" value="true" />
   </appSettings>

In the ProcessRequest method of the handler, I then check this flag:

bool   secure = false ;
string   shouldSecure = ConfigurationSettings.AppSettings["MFRSecure"] ;

if ( null != shouldSecure )
   secure = Convert.ToBoolean ( shouldSecure ) ;

string assembly = context.Request.QueryString["assem"] ;
string image = context.Request.QueryString["image"] ;

if ( secure )
{
   assembly = Crypto.Decrypt ( assembly ) ;
   image = Crypto.Decrypt ( image ) ;
}

ManifestImageLoader.RenderImage ( assembly , image ) ;

Similarly, in the ConstructImageURL method presented earlier, I encrypt the assembly and image names before they are passed to the client.

There are a number of areas where the code could be extended or improved. Here are a few suggestions for you.

  • Rather than hard-coding the image used when a resource is not found, a configuration item could specify the URL of the image. Then when an exception was raised you could load a particular image off disk (or from another assembly!) and return that to the browser.
  • The cache timeout for images could also be defined as a configuration item.
  • The code could be extended to permit any type of image to be served from an assembly—currently the mime type is hard-coded to image/GIF.
  • There's no reason why the code in this example could not serve other resources held within assemblies—you could expose TXT files, WAV files, and so on.

Conclusion

This article has presented two methods for retrieving an image from an assembly in a format suitable for inclusion on a Web site. The first method—serving an image from an ASPX page—is simple and requires no modifications to the Web server configuration; however, the path to the ASPX page that is serving the image needs to be correct in order for the image to be displayed.

The other method—serving an image from a custom handler—overcomes this path-based limitation, but does require alterations to the IIS metabase in order to permit the .mfr extension to be served by the aspnet_isapi.dll extension. It also requires a modification to the web.config for a given application.

My personal suggestion would be to use the handler method rather than the ASPX method, because once it is configured on a Web server it is easier to use (given the fact that a path is unnecessary).

About the Author

Morgan is an Application Development Consultant working for Microsoft in the UK, specializing in Visual C#, controls, WinForms, and ASP.NET. He's been working with .NET since the PDC release in 2000, and liked it so much he joined the company. His home page is http://www.morganskinner.com, where you'll find links to some of the other articles he has written. In his spare time (which there isn't much of) he fights weeds on his allotment and enjoys the odd Cornish pasty.