IIS 7.0

Enhance Your Apps with the Integrated ASP.NET Pipeline

Mike Volodarsky

Code download available at: PHP and IIS 2008_01.exe(171 KB)

This article is based on a prerelase version of the IIS 7.0 FastCGI component. All information herein is subject to change.

This article discusses:

  • ASP.NET Integrated mode
  • Adding user authentication
  • Enabling search-engine-friendly URLs
  • Upgrading performance with output caching
This article uses the following technologies:
IIS 7.0, .NET Framework

Contents

IIS and PHP
Setting Up the App
Securing the Gallery
Forms Authentication
Search-Engine-Friendly URLs
Generating Friendly Hyperlinks
Testing Performance
ASP.NET Output Caching
Deployment and Testing
Wrapping Up

Almost a year ago I wrote an overview of IIS 7.0 in MSDN® Magazine (see "IIS 7.0: Explore the Web Server for Windows Vista and Beyond" at msdn.microsoft.com/msdnmag/issues/07/03/IIS7). That was several months before IIS 7.0 was released with Windows Vista®. Since then, users have had a chance to experience the new IIS 7.0 componentized and extensible architecture and other improvements firsthand.

In the time since Windows Vista shipped, we've been hard at work making sure IIS 7.0 will be a reliable and secure Web server in Windows Server® 2008, honing its stability, performance, and support for hosting environments. We've also taken a hard look at what it means for IIS 7.0 to be a flexible Web application platform. Besides being an excellent platform for Microsoft application frameworks like ASP and ASP.NET, we wanted it to be a great platform for a wide variety of other application frameworks in use today. To facilitate this, we added support for FastCGI, an open Web server standard that allows application frameworks such as PHP, Ruby on Rails, and Perl to be hosted on IIS. We also collaborated with Zend Technologies, the creator of PHP, to bring a solid, high-performance PHP implementation to Windows® and IIS.

IIS 7.0 goes beyond simply providing production support for popular application frameworks. With its new Microsoft® .NET Framework extensibility model, IIS 7.0 can leverage the power of ASP.NET integrated mode to add key functionality to applications developed with any framework. This can enable you to add cool features such as access control or new URL schemes, or dramatically improve performance, often without touching a single line of code.

In this article, I am going to dive deep into the IIS 7.0 ASP.NET integration features to enhance an application that was not developed with ASP.NET in mind. I'll show you how it's possible to both augment an application with existing ASP.NET features and add to the app by developing new features that take advantage of IIS-level ASP.NET extensibility.

The application in question is a popular PHP image gallery application called Qdig (qdig.sourceforge.net). Without so much as touching a single line of PHP code, I'll show you how to add some handy new features to the image gallery. First, I will password-protect the gallery using the ASP.NET membership and forms authentication features. I will also upgrade it to use search-engine-friendly URLs instead of unsightly, parameterized query-string URLs. Finally, I will use the ASP.NET output cache to dramatically improve the performance of the application.

But first, let's look at some history behind the new PHP support in IIS, which is at the core of allowing application frameworks like PHP to enjoy the full set of IIS 7.0 features.

IIS and PHP

IIS had always supported PHP, but in a way that precluded many real-life PHP applications from being hosted in production environments. This was due to limitations in the two ways IIS offered for running PHP applications: using the Common Gateway Interface (CGI) protocol or using the PHP ISAPI extension.

Because CGI requires a separate process for each request, apps hosted using CGI would perform poorly on Windows. Conversely, PHP apps using the IIS high-performance multithreaded ISAPI interface would often suffer from instability due to the lack of thread safety in some popular PHP extensions.

In an attempt to solve these problems, the IIS team developed the FastCGI component. The open FastCGI protocol allows PHP and many other application frameworks that require a single-threaded environment (including Ruby on Rails, Perl, and Python) to run more reliably on IIS. Unlike the standard CGI implementation, FastCGI enables process reuse by maintaining a pool of worker processes, each processing no more than one request at a time, thus resulting in much-improved performance. FastCGI also benefitted from a community-centric development and testing model.

At the same time, Zend Technologies worked to improve the general performance and stability of the PHP scripting engine and core extensions on Windows, making numerous performance improvements and fixing dozens of Windows-specific bugs.

As I am writing this, php.net/downloads offers PHP 5.2.3, the third release of PHP optimized for Windows hosting, with fast non-thread-safe builds optimized for the IIS FastCGI platform. The IIS FastCGI support is built into Windows Server 2008 starting with the Beta 3 release, and is available as a separate Tech Preview download for Windows Vista, Windows XP, and a Go-Live release for Windows Server 2003 (iis.net/fastcgi). When Windows Vista SP1 ships, this component will also be available as part of the installation package. In the near future, final versions for Windows XP and Windows Server 2003 will be released as well. You can read more about the options you have for running IIS FastCGI today at mvolo.com/blogs/serverside/archive/2007/10/09/IIS-FastCGI-and-PHP_3A00_-What-you-absolutely-need-to-know-to-host-PHP-applications-on-IIS-6-and-IIS-7.aspx.

Setting Up the App

IIS 7.0 with FastCGI makes setting up a PHP application pretty simple. To get started, I created a Web site and added a myphpgallery host header binding so it could be accessed as http://myphpgallery from my local machine. (I also added the myphpgallery hostname to %windir%\system32\drivers\etc\hosts so my machine knew where to find it.)

Next, I downloaded the latest version of Qdig from qdig.sourceforge.net and unzipped it into the Web site root. To prepare for testing the gallery, I dropped a bunch of images into the root of the Web site (you can also create subdirectories and drop images in there as well). In this case, I used some of the sample images included with Windows Vista.

I downloaded the latest non-thread-safe PHP build for Windows (PHP 5.2.3 as we go to press), from php.net/downloads and unzipped it to C:\php. I was pretty much good to go at this point, although I needed to make a few tweaks to php.ini that are required for Qdig:

  1. Rename php-recommended.ini to php.ini
  2. Set "register_long_arrays=On"
  3. Enable the GD extension with "extension=php_gd2.dll"
  4. Set the correct extension path with "extension_dir=./ext"

From the IIS perspective, allowing the PHP app to run requires only a couple of steps. First, add the PHP/FastCGI handler mapping to the PHP-CGI.EXE, as described in the following articles: for Windows Vista: go.microsoft.com/fwlink/?LinkId=104195 and for Windows Server 2008 and Windows Vista SP1: go.microsoft.com/fwlink/?LinkId=104196. Then add index.php as the default document.

Finally, because Qdig generates thumbnails on the fly, I needed to grant Write access to the qdig-files subdirectory of the application to the IIS_IUSRS group in order to allow the IIS worker process to write to it.

That's it. At this point I could hit http://myphpgallery and see the images as shown in Figure 1.

Figure 1 The Qdig Gallery

Figure 1** The Qdig Gallery **(Click the image for a larger view)

Securing the Gallery

Qdig is a simple image gallery focused on doing one thing really well: letting you browse your image collection via the Web. As such, it doesn't have some of the more complex features that are often needed for Web applications. One such feature is access control, which allows you to restrict access to parts or all of your Web application.

Unfortunately, implementing access control in PHP requires implementing a credential store and cookie-based authentication from the ground up, and it can be quite hard to get right. By contrast, ASP.NET makes access control a breeze by providing a complete solution with the membership service and a set of built-in credential store providers, the forms authentication module, and a set of pre-made Login controls.

In previous versions of IIS, this wouldn't mean much for the Qdig gallery, since it is not an ASP.NET application. But in IIS 7.0, the ASP.NET Integrated mode engine is designed specifically for this scenario, allowing ASP.NET features to be used with any content, including other application frameworks. Using the functionality of ASP.NET Integrated mode, the Qdig gallery can take advantage of the ASP.NET membership, forms authentication, and Login controls just like a native ASP.NET application. In fact, if you already have an existing ASP.NET application that uses forms authentication, you can simply drop in the Qdig gallery and, with just a few steps, enable it to have the same user security as the rest of your application. This results in consistent user security across your entire site.

To implement a forms authentication solution from scratch for the gallery, I first had to choose a membership credential store provider to store user credentials. ASP.NET 2.0 comes with two built-in providers, one for SQL ServerTM and one for Active Directory®.

The SQL Server provider can be used with SQL Server 2005 Express Edition, which is a great option for many applications. The best thing about using SQL Server 2005 Express Edition with the membership provider is its ability to automatically create and deploy the database tables to your application's App_Data subdirectory on first use. This doesn't require any external database deployment and produces a credential store database that you can Xcopy with your application files.

The SQL Server membership provider is, by default, configured to use a connection string that uses the local SqlExpress instance to connect to the aspnetdb.mdf database inside the App_Data directory for your application. To make it work, I simply took the following three steps:

  1. Downloaded and installed SQL Server 2005 Express Edition, leaving it running as a named SqlExpress instance (default). (You can download it at microsoft.com/sql/editions/express.)
  2. Created an App_Data subdirectory in your application root, and made it writable by IIS_IUSRS.
  3. Opened IIS Manager and created a few users for the application. To do this, select your Web site in the left-hand tree view, and click the .NET Users feature icon (see Figure 2). Be sure to close the tool after you are finished, as SQL Server Express Edition allows only one user identity to access the database at a time.

Figure 2 Configuring Users in IIS Manager

Figure 2** Configuring Users in IIS Manager **(Click the image for a larger view)

Note the integration between IIS Manager and the ASP.NET membership infrastructure—you can manage both users and roles directly from the administration tool where you configure other IIS authentication and access control features. In fact, when you create the first user, IIS Manager will automatically create the membership database in your application if one doesn't already exist.

Forms Authentication

Now that membership is ready to go, I need to enable forms authentication for the application. I will also need to allow the forms authentication module that implements the authentication service to run for all requests in the application, not just for ASP.NET requests (this is the default behavior for backwards compatibility). I can do all of this directly in the Web site's web.config file as follows:

<configuration>
    <system.webServer>
        <modules>
            <remove name="FormsAuthentication" />
            <add name="FormsAuthentication" 
             type="System.Web.Security.FormsAuthenticationModule" />
        </modules>
    </system.webServer>
    <system.web>
        <authentication mode="Forms" />
    </system.web>
</configuration> 

Note that even though the FormsAuthentication module is already declared at the server level, it is necessary to remove it and re-declare it to clear the preCondition attribute, which is set to managedHandler by default. This forces the module to only run for requests to managed (ASP.NET) handlers. Since you want forms authentication to protect the PHP app as well, you need to remove this.

At this point, I have configured our application to use forms authentication. However, I did not indicate that any of the content actually requires an authenticated user—I left Anonymous Authentication enabled to allow anonymous access to the site—so the authentication will never be used. In actuality, I only need to allow anonymous access to the login page and require authentication for the rest of the content. I can do this by configuring declarative authorization rules in configuration. The complete configuration in my web.config file is shown in Figure 3.

Figure 3 Web.config

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <!-- The PHP handler mapping -->
        <handlers>
            <add name="PHP via FastCGI" path="*.php" verb="*"                            modules="FastCgiModule" scriptProcessor="
                c:\php\php-cgi.exe" resourceType="Unspecified" />
        </handlers>
        <!-- Add index.php as a default document -->        
        <defaultDocument>
            <files>
                <add value="index.php" />
            </files>
        </defaultDocument>
        <security>
            <!-- Deny access to anonymous users -->
            <authorization>
                <add accessType="Deny" users="?" />
            </authorization>
        </security>
        <!-- Let the FormsAuthentication module run for all requests -->
        <modules>
            <remove name="FormsAuthentication" />
            <add name="FormsAuthentication" type=
                "System.Web.Security.FormsAuthenticationModule" />
        </modules>
    </system.webServer>
    <system.web>
        <!-- Turn on Forms Authentication for this application -->
        <authentication mode="Forms" />
    </system.web>
</configuration>

With this configuration, access to the PHP gallery will now be allowed only to authenticated users. Users that are not authenticated will be rejected by the URL authorization feature and redirected by forms authentication to the login page where they can log in using the membership service and the SQL Server credential database.

The final piece of the jigsaw puzzle is the login page, which ties all of this together by providing a login Web form, validating the user's credentials with the Membership service, and issuing the user an encrypted forms authentication login ticket. If you are starting to roll up your sleeves at this point and looking around for your coding hat, don't; this login page is pretty much already done for you. All you need to do is create a simple page in your application root called login.aspx (you can override the path to the login page in the <forms> configuration section). This newly created page uses a single Login control:

<html>
  <head>
    <title>Login to my blog</title>
  </head>
  <body>
    <form runat="server">
      <asp:Login runat="server" />
    </form>
  </body>
</html>

The Login control will automatically display a login form, validate your user credentials after submission, and issue the forms authentication ticket. This ticket will then be used to automatically authenticate you for the rest of the time on the Web site, until it expires due to inactivity or you close the browser window.

Figure 4 shows a spruced-up version of the login page; it is using a couple of other login-related controls in order to provide a more convenient user experience. The LoginView control allows you to select between an anonymous user's view and an authenticated user's view, with the LoginName control providing the authenticated user's name and the LoginStatus control providing a logout link for authenticated users. You have the ability to specify the way in which the login controls appear to the client by going ahead and setting one of myriad formatting properties or by theming or skinning them with custom CSS.

Figure 4 Fancy Login Page

<html>
  <head>
    <title>Login to view the gallery</title>
  </head>
  <body>
    <form runat="server">
      <asp:LoginView runat="server">
        <AnonymousTemplate>
          Please log in to proceed.
          <br /><br />
          <asp:Login DestinationPageUrl="/"  runat="server" />        
        </AnonymousTemplate>
        <LoggedInTemplate>
          Welcome <asp:LoginName runat="server" />!
          <br /><br />
          <asp:LoginStatus runat="server" />
        </LoggedInTemplate>
      </asp:LoginView>
    </form>
  </body>
</html>

That's it. Now, when you access your gallery, you will be automatically redirected to the login page shown in Figure 5. To log in, you will need to enter user credentials for one of the users you created earlier, and you are in business!

Figure 5 The Completed Login Page

Figure 5** The Completed Login Page **(Click the image for a larger view)

Search-Engine-Friendly URLs

By default, the Qdig image gallery uses ugly-but-functional querystring URLs to navigate through your image gallery. Here is an example of a Qdig URL from my gallery:

http://myphpgallery/index.php?Qwd=./Mike&Qif=Flower.jpg

Not particularly pretty or memorable. More importantly, it makes it more difficult for search engines to properly index the content of your gallery.

What I would like to do is make Qdig use search-engine-friendly URLs, which are all the rage for Web 2.0 applications these days because they create a much more intuitive browsing experience for users and are much more effective at getting your Web site indexed well by search engines. For example, a friendly URL version of the one I showed earlier would be:

http://myphpgallery/index.php/Mike/Flower.jpg

This causes the subgallery and image name to be put into the URL path, making it more intuitive, and the important keywords are placed in the absolute path of the URL, making it more easily indexed by search engines.

Interestingly enough, the server will correctly route this URL to index.php by default, recognizing the remaining "/Mike/Flower.jpg" as the PATH INFO segment. However, the problem with this is that this URL does not follow the URL format expected by the Qdig application's script, index.php. Even though we can get the URL to invoke the index.php script, Qdig will not find the required querystring parameters and won't know how to work correctly.

Fortunately, the ASP.NET Integrated pipeline comes to the rescue again, this time allowing me to write a small .NET module that can rewrite incoming URLs on the fly from a more friendly format to Qdig-native URLs, causing the correct gallery view to be returned to the client. Thanks to the ASP.NET Integrated pipeline, this module can run for PHP requests the same way it can for requests to ASP.NET content.

To do this, I will implement a managed module using the ASP.NET IHttpModule pattern. You can read more about building such modules for IIS 7.0 in the article "Developing IIS7 Modules and Handlers with the .NET Framework" at mvolo.com/blogs/serverside/archive/2007/08/15/Developing-IIS7-web-server-features-with-the-.NET-framework.aspx.

The module will do the following:

  1. Subscribe to the BeginRequest stage of request processing.
  2. Intercept requests with URLs in a friendly format.
  3. Extract the subgallery path and the image file name from the URL.
  4. Rewrite the requests to the Qdig index.php, passing the subgallery path and image file name in querystring parameters as expected by Qdig.

Writing this module was actually quite simple. First, I needed to create a C# class that implements the System.Web.IHttpModule interface and wires up an event handler inside the Init method to the BeginRequest event:

public class QdigSEFModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.BeginRequest += new EventHandler(OnBeginRequest);
    }
    ...
}

Then I implement the rewriting functionality in the OnBeginRequest method as shown in Figure 6.

Figure 6 Rewriting URLs

public void OnBeginRequest(Object source, EventArgs e)
{
    HttpApplication app = (HttpApplication)source;
    HttpContext context = app.Context;

    // Extract the gallery path and image filename from the url
    // code omitted for clarity ...

    // Build the rewritten url
    String rewrittenUrl = String.Format("~/index.php?Qwd=.{0}&Qif={1}",
                                        galleryPath,
                                        imageFileName);

    String originalQueryString =      
             context.Request.ServerVariables["QUERY_STRING"];
    if (!String.IsNullOrEmpty(originalQueryString))
    {
        rewrittenUrl += "&" + originalQueryString;
    }

    // Rewrite the url
    context.Server.TransferRequest(rewrittenUrl);
}

Note the use of HttpServerUtility.TransferRequest, a new ASP.NET API in the .NET Framework 3.5 intended to allow correct rewriting in the IIS 7.0 ASP.NET Integrated pipeline. This API forces the request processing to be stopped for the current request and executes a new child request to the target URL. This allows managed modules to completely transfer request processing to another URL, regardless of the destination content type. You can find the complete source code for this module in the download for this issue.

After writing the module, I need to deploy it to the application. There are a number of ways to do this, including compiling the module into a .NET assembly and deploying it to the BIN directory of the application. However, I will pursue the easiest option: simply copying the source code of the module and saving it as QdigSEFRewriter.cs in the App_Code subdirectory of the application. The ASP.NET compilation system will automatically compile the code during application startup and make it available to the ASP.NET runtime.

The final step is to enable the module to run in the application by registering it in the <modules> configuration section of the application's web.config file, where a number of built-in ASP.NET modules such as forms authentication are already registered by default. Building on the web.config file created for access control, I can add the new module like so:

<system.webServer>
    <modules>
        <remove name="FormsAuthentication" />
        <add name="FormsAuthentication" type=
            "System.Web.Security.FormsAuthenticationModule" />
        <!-- Add the SEF url rewriting module -->
        <add name="QdigSEFUrlsModule" type="QdigModules.QdigSEFModule" />
    </modules>
</system.webServer>

At this point, you can begin accessing the Qdig gallery using friendly URLs instead of the Qdig native querystring URLs. Now, to access the Flower.jpg file in the Mike subdirectory of the gallery, I can simply request the file like this:

http://myphpgallery/Mike/Flower.jpg/show

Notice that I chose to use the /show suffix for friendly URLs to differentiate them from direct links to the image files and to prevent my module from rewriting URLs that it is not intended to rewrite. You can, of course, modify the rewriting scheme in a way that suits your needs.

Generating Friendly Hyperlinks

Unfortunately, I'm not quite finished. While galleries can now be accessed with friendly URLs, the gallery pages themselves still contain hyperlinks in the old format:

<a href="http://index.php?Qwd=./Mike&amp;Qif=Arctica.jpg&amp;Qiv=thumbs&amp;
Qis=M" title="First Image"> ... </a>

A user clicking on these links will still see the unsightly querystring URLs, and a search engine indexing the content will not be able to correlate the friendly URLs for the parent page with the links in the page body.

It would be really handy to make Qdig generate hyperlinks using the same friendly URLs that I enabled in the previous step. But doing this would entail rewriting the PHP script to generate different hyperlinks, which is something I didn't want to do.

It turns out that I don't have to, thanks to the ASP.NET response filtering facility. I can write another managed module that filters the Qdig responses on the fly, replacing the hyperlinks in the old format with links in the new, friendly format.

This time, my module will do the following:

  1. Subscribe to the PreRequestHandlerExecute stage of request processing (just before the handler executes).
  2. Check whether the URL is for the Qdig index.php script.
  3. Set the response filter stream on the response, which will use regular expressions to replace all occurrences of friendly hyperlinks.

The module class is pretty simple, since all it does is selectively wire up the filter stream (see Figure 7). The real work is done in the QdigSEFFilter class, which is responsible for replacing all occurrences of Qdig hyperlinks with hyperlinks using the new format. The filter class implements the abstract System.IO.Stream class and is used by the ASP.NET runtime to filter the response at the end of request processing. The ASP.NET Integrated pipeline engine allows the response filter to operate on any response, even those not generated by ASP.NET, and so can be used to process PHP responses as well as static files and ASP pages as long as the response is buffered. IIS 7.0 buffers all responses by default (up to the configured limits), which allows this mechanism to work.

Figure 7 Generating Friendly Hyperlinks

public class QdigSEFFilterModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.PreRequestHandlerExecute += new EventHandler(
            OnPreRequestHandlerExecute);
    }

    public void Dispose(){}

    public void OnPreRequestHandlerExecute(Object source, EventArgs e)
    {
        HttpApplication app = (HttpApplication)source;
        HttpContext context = app.Context;

        if (context.Request.Path.Equals(
                VirtualPathUtility.ToAbsolute("~/index.php")))
        {
            // wire up the response filter
            context.Response.Filter = new QdigSEFFilter(context);
        }
    }
}

My filter implementation will buffer all of the incoming response bytes pushed through its stream, convert them to a string using the charset of the response, and perform the regular expression replacement of URLs. Then it will re-encode the string into the original charset and push the response data back to the runtime. You can find the full implementation of the module and the response filter in the code download.

I will again deploy the module code as a separate source file, QdigSEFFilter.cs, into the App_Code directory, and register the module in the <modules> configuration section in the application's web.config. At this point, the modules section will look like this:

<system.webServer>
    <modules>
        <remove name="FormsAuthentication" />
        <add name="FormsAuthentication" type=
            "System.Web.Security.FormsAuthenticationModule" />
        <!-- Add the SEF url rewriting module -->
        <add name="QdigSEFUrlsModule" type="QdigModules.QdigSEFModule" />
        <!-- Add the response filter module -->
        <add name="QdigSEFFilterModule" type=
            "QdigModules.QdigSEFFilterModule" /> 
    </modules>
</system.webServer>

After refreshing the Qdig page at http://myphpgallery/ and logging in, all of the hyperlinks on the page have changed to use the friendly URLs (see Figure 8). So far, I haven't had to change a single line of the original application, after making several significant upgrades to its functionality with existing ASP.NET features and new functionality developed for the ASP.NET Integrated pipeline.

Figure 8 The Revised Gallery with Friendly URLs

Figure 8** The Revised Gallery with Friendly URLs **(Click the image for a larger view)

Testing Performance

At this point, I have added quite a bit of functionality to Qdig, even though I haven't changed any of the application's source code. So how much does all this new functionality impact the performance of the application? Performance has historically been a concern for PHP applications on the IIS platform due to the process-launching overhead of the CGI mechanism on Windows. FastCGI support promises to significantly improve application performance by removing this overhead, making IIS a much more attractive platform for hosting PHP and other FastCGI-compliant applications. However, if the application enhancements I just introduced add a significant portion of this overhead back, they may not be so attractive after all when considering deploying PHP applications on IIS.

In reality, however, the feature enhancements I added have little effect on Qdig's performance. Figure 9 shows a summary of the performance tests I performed on the application.

Figure 9 Performance Results

Test Requests per Second (more is better)
Qdig with CGI 32
Qdig with FastCGI 93
Qdig and friendly URLs with FastCGI 88
Hello.php with CGI 51
Hello.php with FastCGI 2239

The test was performed with the Microsoft Web Capacity Analysis Tool (WCAT) 6.3 using 100 concurrent virtual clients with full CPU utilization on the server, requesting a series of Qdig URLs inside the gallery. For the last test, friendly URLs were used instead of Qdig URLs to utilize the conversion functionality. Authentication was disabled for ease of testing.

Moving from CGI to FastCGI, you see an improvement of almost three times the throughput. After adding the two friendly URL modules, I only saw a 5 percent drop in throughput, which is negligible.

Unfortunately, Qdig is extremely CPU-bound, which negates the bulk of the possible improvement provided by FastCGI. By comparison, for a simple hello.php script, I saw an improvement of 43 times the throughput when going from CGI to FastCGI. Even after adding a response filter—a pretty expensive operation—the impact on overall throughput is negligible in comparison with the overhead of the application itself.

This is pretty much the worst situation to be in as far as server performance tuning is concerned, because there is very little you can do outside of the application to improve its performance. Changing the application code itself is often impossible unless you are the application's developer, and even then it may be difficult. For me it's certainly out of scope as I decided not to modify the application itself when I started.

ASP.NET Output Caching

The output cache is a feature of ASP.NET that allows reuse of responses produced by Web applications for subsequent requests to the same resources. In fact, ASP.NET provides an extensive set of features for caching both application data inside ASP.NET applications with its Cache API and an application's entire responses with the output cache. Both features can often enhance an application's performance and significantly reduce the load on the back-end resources. New in IIS 7.0 is the ability to use the ASP.NET output cache for non-ASP.NET content types, which is exactly what I will use to give the image gallery a little boost.

As opposed to piecemeal application performance tuning that often requires a large coding effort and removes chunks of the application's processing overhead, output caching tends to remove virtually the entire overhead for a percentage of the request workload. Its effectiveness depends on the locality of the application's usage: how often the same content is requested by the application's users. For typical applications, this often falls into the 90/10 rule, which basically means that 90 percent of requests always go to 10 percent of your content. If you can output-cache this content, it may mean a near 90 percent throughout and capacity improvement to your application.

Of course, if it was so easy to do, everyone would be doing it. Output caching does have a set of limitations, especially when it comes to dynamic content, which often makes deploying a caching solution difficult. The major limitations stem from the dynamic nature of the content. For example, your Web site's pages may produce different responses based on query-string parameters, time of day, or database updates. If you neglect to take these into account, you may return incorrect cached responses to your users, which will not be a good thing. Fortunately, the ASP.NET output cache takes all of these limitations into account and as a result provides a rich platform for deploying output caching solutions for your application.

The ASP.NET page parser provides support for the<%@ OutputCache %> directive, which allows many output caching settings to be configured directly in the ASPX page. In fact, this is how most developers enable output caching for their ASP.NET applications. Since Qdig is written in PHP, it can't take advantage of this feature. So instead I will write another module that dynamically configures the output cache using the ASP.NET HttpCachePolicy APIs, enabling the output cache module to correctly cache Qdig's responses:

public class QdigOutputCacheModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.PostAuthorizeRequest += 
            new EventHandler(OnPostAuthorizeRequest);
        application.PostRequestHandlerExecute += 
            new EventHandler(OnPostRequestHandlerExecute);
    }

    public void Dispose(){}
}

This time, this module will subscribe to two pipeline events: PostAuthorizeRequest and PostRequestHandlerExecute. The first, PostAuthorizeRequest, occurs right before the output cache module attempts to look up an existing response for the request and return it without further processing. The PostRequestHandlerExecute event occurs right after the handler (PHP) produces the response and before the output cache module attempts to save the response for subsequent use.

In the PostAuthorizeRequest event, I need to tell the output cache module how the Qdig responses vary by querystring parameters, so it can find the right response (see Figure 10). By configuring the cache to vary by the querystring parameters, I am enabling the Qdig gallery to operate correctly by responding to the user input such as changing the navigation mode or clicking the next link to go to the next image. Without this, the first request will end up caching its response and returning it for all subsequent requests to the gallery regardless of which image was requested. Also, I will vary by the authenticated user (see the GetVaryByCustomString implementation further), so that the cache correctly gives each authenticated user his own cached copy.

Figure 10 Determining the Correct Cache Variation

public void OnPostAuthorizeRequest(Object source, EventArgs e)
{
    HttpApplication app = (HttpApplication)source;
    HttpContext context = app.Context;

    // Make sure we only process requests to QDIG's index.php.
    String requestPath = context.Request.Path;
    if (!requestPath.Equals(VirtualPathUtility.ToAbsolute
        ("~/index.php")))
        return;

    //  Set up the vary by querystring information 
    HttpCacheVaryByParams varyByParams = 
      context.Response.Cache.VaryByParams;
    varyByParams["Qwd"] = true; //  gallery path
    varyByParams["Qif"] = true; //  image to display
    varyByParams["Qiv"] = true; //  navigation mode
    varyByParams["Qis"] = true; //  image size
    varyByParams["Qtmp"] = true;//  other control information

    // if user is set, also vary by user (See global.asax)
    context.Response.Cache.SetVaryByCustom("AuthenticatedUser");
}

In the PostRequestHandlerExecute event, I need to determine whether the response should be cached and configure the appropriate settings allowing it to be cached (see Figure 11). Note the AddFileDependency calls. In the omitted code, I determine the physical path to the gallery directory and the image, if specified, and add a File Dependency to the response. This uses the CacheDependency mechanism supported by the output cache to monitor these resources for changes and to invalidate the cached response when they are modified. This allows me to keep the response cached for a long time, only invalidating it when the underlying data changes. When someone drops a new image or creates a new subgallery, the cache automatically removes the affected cached responses and any new requests will receive the updated pages.

Figure 11 Configuring the Response to be Cached

public void OnPostRequestHandlerExecute(Object source, EventArgs e)
{
    HttpApplication app = (HttpApplication)source;
    HttpContext context = app.Context;

    // Make sure we only process requests to QDIG's index.php.
    String requestPath = context.Request.Path;
    if (!requestPath.Equals(VirtualPathUtility.ToAbsolute
        ("~/index.php")))
        return;

    // Enable this response to be output cached
    context.Response.Cache.SetCacheability(HttpCacheability.Public);
    context.Response.Cache.SetExpires(DateTime.Now + 
      TimeSpan.FromMinutes(5));

    // code omitted for clarity ...

    // add the dependencies to the response
    context.Response.AddFileDependency(physicalDirectoryPath);
    context.Response.AddFileDependency(physicalFileName);
}

The CacheDependency mechanism is a powerful abstraction, allowing you to create your own CacheDependency implementations that can monitor any kind of underlying data—for example, making a Web service call or checking registry keys. The built-in CacheDependency implementations include support for monitoring files, directories, and SQL Server database tables.

Deployment and Testing

I deployed the latest module by dropping its QdigOutputCache.cs source file into the App_Code directory, just like I did before, and registered it in web.config:

<system.webServer>
    <modules>
      ... 
      <!-- Let the OutputCache module run for all requests -->
      <remove name="OutputCacheModule" />
      <add name="OutputCacheModule" type=
          "System.Web.Caching.OutputCacheModule" /> 
      <!-- Add the custom output caching module --> 
      <add name="QdigOutputCacheModule" type=
          "QdigModules.QdigOutputCacheModule" /> 
    </modules>
</system.webServer>

Besides adding the QdigOutputCacheModule, I also re-added the OutputCacheModule in order to remove its managedHandler precondition as I did before for forms authentication. This enables the ASP.NET OutputCacheModule to run during requests to PHP content and provide the output caching service.

Finally, I created a global.asax file (see Figure 12) that contains my implementation of the GetVaryByCustomString, a function that allows my module to vary the cached response by the authenticated user. This is important, because it maintains separate cache copies for each authenticated user, preventing one user from accidentally getting another user's personalized view. Since Qdig does not provide personalized views, this isn't an issue, but I show it anyway as a best practice.

Figure 12 Global.asax Implements GetVaryByCustomString

<%@ Application language="C#" %>
<script runat="server" language="c#">
public override string GetVaryByCustomString(HttpContext context, 
    string s)
{
    if ("AuthenticatedUser".Equals(s))
    { 
        // vary by authenticated user
        String currentUser = String.Empty;
        if (context.User != null)    
        {
            currentUser = context.User.Identity.Name;
        }

        return currentUser;
    }
    else
    { 
        // unknown vary string
        return null;
    }
}
</script>

Now let's check whether performance was further improved. The complete results, including the previous tests, are shown in the graph in Figure 13.

Figure 13 Performance Results

Figure 13** Performance Results **(Click the image for a larger view)

As expected, output caching did not disappoint, raising the performance of Qdig from 88 requests per second in the baseline FastCGI plus search-engine-friendly (SEF) URL benchmark, to 1386 requests per second on a fully utilized server.

All in all, after moving a native PHP application from CGI to FastCGI, and upgrading it with friendly URLs and output caching, the end result shows a performance improvement of over 40 times the original application. And that is certainly nothing to scoff at.

Wrapping Up

As you can see, the IIS 7.0 Integrated pipeline allows you to add powerful functionality to any application, bringing the power and convenience of ASP.NET and the .NET Framework to apps written on other frameworks. For development teams, this ability is key to leveraging investments in existing applications and developer skill sets. Instead of forcing the applications to be rewritten in order to move to the new platform, IIS 7.0 allows existing apps to benefit from incremental improvements brought about by existing IIS and ASP.NET features, as well as new features developed using the IIS 7.0 end-to-end extensibility model.

In this article, I barely scratched the surface of what is possible with the IIS 7.0 extensibility APIs. While I looked exclusively at the runtime Web server extensibility, IIS 7.0 also provides complete extensibility at the configuration, administration, and GUI management levels, creating myriad exciting opportunities to upgrade and mold your Web server to your needs. To learn more about extending IIS 7.0, as well as download a number of ready-to-use IIS 7.0 plug-ins, be sure to check out my blog, mvolo.com.

Mike Volodarsky is a Technical Program Manager on the Web Platform and Tools Team at Microsoft. Over the past four years, he has driven the design and development of the core feature-set for ASP.NET 2.0 and IIS 7.0. He is now focusing on helping customers leverage the power of these technologies in Windows Server 2008.