Happy Hunting

Extend the ASP.NET page output cache and get inside AspCompat.

Ask the Pro

LANGUAGES: C#

TECHNOLOGIES: Page Output Caching | COM Interop

 

Happy Hunting

Extend the ASP.NET page output cache and get inside AspCompat.

 

By Jeff Prosise

 

Q: ASP.NET's @ OutputCache directive enables you to cache different versions of a page based on varying request headers and input parameters. I need to cache a unique version of a page for each different session in which the page is rendered. Is that possible? If so, how?

 

A: This is quite possible for the simple reason that ASP.NET's designers had the foresight to make the ASP.NET page output cache as extensible as it is easy to use. First, I'll provide a quick review followed by the answer to the question.

 

Out of the box, the @ OutputCache directive can cache different versions of a page to reflect variations in input parameters, request headers, and browser types. VaryByParam determines how ASP.NET responds to variations in user input. The following statement caches two different versions of the host page if one request contains a FavoriteColor parameter equal to Red and another contains a FavoriteColor parameter equal to Blue (the sample code referenced in this article is available for download):

 

<%@ OutputCache Duration="60"

  VaryByParam="FavoriteColor" %>

 

The VaryByHeader attribute controls how ASP.NET reacts to varying request headers. Suppose you author a page that uses Accept-Language headers to display English text for U.S. users and French text for French users; you can use the VaryByHeader attribute to cache a different version of the page for each different Accept-Language header encountered:

 

<%@ OutputCache Duration="60"

  VaryByHeader="Accept-Language"

  VaryByParam="None" %>

 

Finally, you can use the VaryByCustom attribute to cache a unique snapshot of the page for each different browser type (determined by the name and major version number) from which requests emanate:

 

<%@ OutputCache Duration="60"

  VaryByCustom="Browser"

  VaryByParam="None" %>

 

VaryByCustom also happens to be the key to customizing ASP.NET's page output caching strategy. Assigning a value to VaryByCustom in an @ OutputCache directive causes ASP.NET to call the GetVaryByCustomString method of the HttpApplication object associated with the current request. ASP.NET caches a different version of the page for each unique string returned by GetVaryByCustomString. Setting VaryByCustom to Browser works because the default implementation of GetVaryByCustomString looks something like this:

 

public virtual string

    GetVaryByCustomString (HttpContext context, string arg)

{

      if (arg.ToLower () == "browser") {

          return context.Request.Browser.Type;

      return null;

}

 

The fact that you can override GetVaryByCustomString in Global.asax provides the key to customizing the caching strategy. The page generated from the code in Figure 1 uses this knowledge to cache different versions of a page based on varying session IDs. The page itself does little more than output its own session ID (if present) and the current wall clock time. But it also sets VaryByCustom to "SessionID," which enables the GetVaryByCustomString method - found in Figure 2 - to vary the output cache by returning the session ID. See for yourself: Deploy these files to a virtual directory, start two browser instances using each to call up the ASPX file in Figure 1, and hit refresh a couple of times. (The page won't be cached the first time because the request lacks a session ID cookie.) You will see that the page is cached for 10 seconds at a time (note that the time only updates every 10 seconds) and that each browser instance shows only its own session ID - proof positive that our customized caching strategy is working.

 

<%@ Page Language="C#" %>

<%@ OutputCache Duration="10" VaryByParam="None"

  VaryByCustom="SessionID" %>

 

<html>

  <body>

     <%

      Session["MyData"] = "Foo";

      Response.Write ("Your session ID is " +

          Session.SessionID);

      Response.Write ("<br>");

      Response.Write ("The current time is " +

          DateTime.Now.ToLongTimeString ());

    %>

  </body>

</html>

Figure 1. The @ OutputCache directive's VaryByCustom attribute allows different versions of a page to be cached based on criteria you define.

 

<script language="C#" runat="server">

public override string

    GetVaryByCustomString (HttpContext context, string arg)

{

    if (arg.ToLower () == "sessionid") {

        HttpCookie cookie =

            context.Request.Cookies["ASP.NET_SessionId"];

        if (cookie != null)

            return cookie.Value;

    }

    return base.GetVaryByCustomString (context, arg);

}

</script>

Figure 2. The GetVaryByCustomString method in this Global.asax file returns the session ID - if any - accompanying the request, forcing a different version of the page into ASP.NET's page output cache for each session in which the page is rendered.

 

A variation on this technique modifies the output cache based on userids. If you authenticate callers to your site and customize output based on Page.User.Identity.Name, you need to cache different snapshots for different users when utilizing page output cache. This directive is the first step toward implementing a customized caching strategy based on user identities:

 

<%@ OutputCache Duration="60"

  VaryByCustom="UserID"

  VaryByParam="None" %>

 

The second step is overriding HttpApplication.GetVaryByCustomString and implementing it to recognize "UserID" and return the user's name if the user indeed has been authenticated. Because it should be obvious now how to do that, I will leave the implementation up to you.

 

Q: What is AspCompat and when should I use it? Why does the .NET Framework SDK documentation warn that using AspCompat might degrade performance?

 

A: AspCompat is an attribute supported by ASP.NET's @ Page directive. You can set AspCompat to true by including this directive in an ASPX file:

 

<%@ Page AspCompat="true" %>

 

Use the attribute when either - or both - of these conditions is true: If your page uses apartment-threaded legacy ASP COM components - that is, components registered as ThreadingModel="Apartment" or that have no registered ThreadingModel value (note that all COM components written with Visual Basic 6.0 are apartment-threaded); or, if your page uses legacy COM components that use ASP's intrinsic objects (Request, Response, and so on).

 

To make sense of these rules, it helps to understand what happens to ASP.NET when you set AspCompat to true. This means looking under the hood to see how AspCompat works.

 

Setting AspCompat to true has two effects on ASP.NET. The first effect involves the relationship between ASP.NET and COM. By default, ASP.NET uses threads that reside in a COM multithreaded apartment (MTA) to process HTTP requests. Apartment-threaded COM components always run, however, in single-threaded apartments (STAs). Without AspCompat, a call from an ASP.NET page to an apartment-threaded COM component must be marshaled across apartment boundaries from MTA to STA. The call incurs a substantial performance hit as it crosses apartment boundaries, largely due to the thread switch required to marshal a call into an STA.

 

AspCompat="true" configures ASP.NET to process requests for that page with STA-based worker threads. (The threads come from a pool maintained by the COM+ runtime separately from ASP.NET's pool of MTA threads.) When an STA thread creates an STA-based object, both caller and callee reside in the same apartment and no marshaling occurs in calls from one to the other. That's good for performance. Ironically, the SDK docs warn that AspCompat="true" inhibits performance. Although it's true that ASP.NET performs better with MTA threads than STA threads in the absence of COM components, pages that use STA COM components perform much better on STA threads than on MTA threads.

 

To see the performance difference for yourself, download the sample code for this article - which includes an ASP COM component you can use for testing - and perform these six steps:

 

1. Extract Sieve.dll from the downloadable zip file. Sieve.dll is a COM DLL containing an ASP test component whose ProgID is "ASP.Sieve".

 

2. In a Visual Studio .NET command prompt window, navigate to the directory where Sieve.dll resides and execute this command to register the test component on your system:

 

        regsvr32 sieve.dll

 

3. While still in the command prompt window, execute this command to generate a runtime-callable wrapper (RCW) for the test component:

 

        tlbimp sieve.dll

 

This step is necessary because you'll be calling the test component both with and without AspCompat="true", and ASP.NET doesn't allow an apartment-threaded COM component to be called without AspCompat="true" unless it's called through an RCW.

 

4. Copy the interop assembly (SIEVELib.dll) generated by the TLBIMP utility to wwwroot's bin subdirectory. (If wwwroot doesn't have a bin subdirectory, create one.)

 

5. Run the page listed in Figure 3 from wwwroot and click on the Test button. This instantiates the test component and makes 1,000 calls to its CountPrimes method, which computes the number of prime numbers between two and a caller-specified ceiling (in this case, 1,000). Find the "Begin Test" and "End Test" markers in the trace output that flag the beginning and end of the 1,000 calls and write down the figure in the "From Last(s)" column of the "End Test" row. The elapsed time - how long it takes to execute all 1,000 calls - is more than 62 milliseconds.

 

6. Add AspCompat="true" to the page's @ Page directive and repeat the previous exercise. Note the reduction in elapsed time between "Begin Test" and "End Test" - it's less than 13 milliseconds!

 

<%@ Page Trace="true" %>

<%@ Import Namespace="SIEVELib" %>

 

<html>

  <body>

    <form runat="server">

      <asp:Button Text="Test" OnClick="OnTest"

        runat="server" />

    </form>

  </body>

</html>

 

<script language="C#" runat="server">

void OnTest (Object sender, EventArgs e)

{

    SieveClass sieve = new SieveClass ();

    Trace.Warn ("Begin Test");

    for (int i=0; i<1000; i++) {

        int count = sieve.CountPrimes (1000);

    }

    Trace.Warn ("End Test");

}

</script>

Figure 3. AspCompat.aspx instantiates an ASP test component using a runtime-callable wrapper (RCW) and places 1,000 calls to it. Data in the trace output reveals the execution time.

 

Was the difference larger than you expected? The roughly five-fold difference in execution times on the server I tested on demonstrates the dramatic performance implications of allowing COM calls to cross apartment boundaries. Sure, the difference was exacerbated by the fact that the page called CountPrimes 1,000 times, but then again, CountPrimes is a CPU-intensive method. The less work a COM object does in a method call, the more significant the time required to call it in a remote apartment becomes.

 

The second effect setting AspCompat to true has on ASP.NET is to make intrinsic ASP objects such as Request and Response available to legacy ASP components. Without AspCompat="true", legacy components cannot access intrinsic ASP objects. Consider this ASP page:

 

<html>

  <body>

    <%

      Set obj = Server.CreateObject ("Test.MyComponent")

      obj.Speak ("Hello, world")

    %>

  </body>

</html>

 

This page instantiates an ASP component named "Test.MyComponent" and calls its Speak method. Speak, in turn, uses ASP's Response.Write to output the string passed to it to the Web page, resulting in a page containing the text "Hello, world."

 

Here's the equivalent page written in ASP.NET:

 

<%@ Page Language="C#" AspCompat="true" %>

<%@ Import Namespace="System.Reflection" %>

 

<html>

  <body>

    <%

      Type t = Type.GetTypeFromProgID ("Test.MyComponent");

      Object test = Server.CreateObject (t);

      Object[] args = { "Hello, world" };

      t.InvokeMember ("Speak", BindingFlags.InvokeMethod,

          null, test, args);

    %>

  </body>

</html>

 

This page will not work if you remove AspCompat="true" because the COM component won't have access to ASP's Response object (or any other intrinsic ASP objects, for that matter). If, in fact, you remove the AspCompat attribute and run the page, ASP.NET will throw an exception warning that AspCompat is required. You can eliminate the exception by registering the COM component ThreadingModel="Free" (hence moving the object into the process's MTA), but the Speak method still won't work because it cannot call Response.Write. In other words: No AspCompat, no intrinsic objects. Believe it.

 

With this background in mind, the two rules for when to use AspCompat should make more sense. Rule 1 is chiefly a performance optimization; it's all about eliminating interapartment method calls. Rule 2 ensures that legacy ASP components that rely on intrinsic ASP objects have access to those objects.

 

As a corollary to Rule 1, realize that if an ASP.NET page uses a legacy ASP component that is not apartment-threaded (that is, a component registered ThreadingModel="Free" or ThreadingModel= "Both"), you should not set AspCompat to true unless the component relies on intrinsic ASP objects for its operation. Calling an MTA-based component from an STA degrades performance just the same as calling an STA-based component from an MTA. If the component requires access to ASP intrinsic objects, however, that degradation is a price you'll have to pay.

 

Incidentally, if much of what you just read here sounds like gobbledygook because you're not familiar with COM apartments, I've provided URLs to a pair of articles I've written explaining MTAs and STAs (see References). Happy apartment hunting!

 

References

 

The sample code referenced in this article is available for download.

 

Jeff Prosise is author of several books, including Programming Microsoft .NET (Microsoft Press). He also is a co-founder of Wintellect (http://www.wintellect.com), a software consulting and education firm that specializes in .NET. Got a question for this column? Submit queries to [email protected].

 

Tell us what you think! Please send any comments about this article to [email protected]. Please include the article title and author.

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish