This post originated from an RSS feed registered with .NET Buzz
by Scott Hanselman.
Original Post: ASP.NET, Caching, and Cartesian Products
Feed Title: Scott Hanselman's ComputerZen.com
Feed URL: http://radio-weblogs.com/0106747/rss.xml
Feed Description: Scott Hanselman's ComputerZen.com is a .NET/WebServices/XML Weblog. I offer details of obscurities (internals of ASP.NET, WebServices, XML, etc) and best practices from real world scenarios.
I'm a HUGE believer in caching and unfolding data. If you have a little extra
RAM on your Web Servers, take advantage of it and cache. When caching on the
Web Server, the form of the data you cache should "look" as much like the data the
end user would see. In other words, if you have VERY normalized data in the
database, but the HTML that will eventually be rendered to the user is a very unfolded,
flat version of that data, then the data you cache should look more like the latter
than the former.
When Patrick Cauldwell, Joe Tillotson,
and Javan Smith worked on 800.com and Gear.com during the boom (800.com was
bought by Circuit City and Gear.com was
bought by Overstock.com) we built a series of multi-level caches that unfolded
(de-normalized) the closer they got to the point of rendering, until we finally cached
rendered HTML.
For sites that follow a regular navigation scheme (often that scheme is described
by an XML file or in a Database...you know, nav.config, etc...we've all written one)
the HTML of the headers, footers and navigation UI element (trees, pulldowns, tabs)
should be cached if they are shared between more than one user. Meaning, that
if every user has a unique navigation, of course the ASP.NET Cache object isn't the
place for them.
In a site I'm working on now, there are (names changed to protect the innocent) Gold and Silver users.
Gold users see one set of navigation tabs, Silvers see another. Additionally
Gold and Silver users can be enrolled in additional programs, like Plan-A and Plan-B.
Whether a user is enrolled in Plan-A or Plan-B is not related to their membership
in the Gold and Silver groups.
Additionally, the navigation tabs are drawn based on the current page (in Request.RawUrl).
Some navigation schemes that I am no longer a fan of are those that include techniques
like Page.aspx?nav=tab1&subnav=subtab4&somethingsecret=somethingsilly.
I prefer to use liberal use of Url Rewriting and "simulated pages," like changing
sitename/book.aspx?isbn=123 to sitename/123.book, etc.
For this site, we are just using page names and indicating in a navigation config
XML file what tabs belong with what page. For example:
...yada, yada, yada. Of course, it's much more complex that this. Each
page also includes context sensitive help, user customizable links from a dropdown
and a list of links that are related to the page their are on that the user may find
interesting. All of these are inter-related, making the XML file fairly normalized
and complicating things. When the file is finally deserialized, a series of
hashtables and lookuptables are cached in memory for efficiency and used when rendering
the menu. The menu can render itself as a series of Tabs and SubTabs or a Tree,
or whatever.
The header/renderer is an ASCX file that asks the "NavigationService" for the details
of the current navigation scheme, based on Context, in this case HttpContext.
A series of tests are done, checking .NET's role-based security for the user's roles.
Gold and Silver are mutually exclusive and Plan-A an Plan-B are not.
That means that given n pages, x mutually exclusive roles and y non-mutually-exclusive
roles, there can be:
and on and on. So, if you look at the 1*2*4=8 strings above, you can imagine
them as keys in a HashTable. We can cache Header two different ways (actually
dozens, but let's make it simple):
1. As Control objects in the Control Tree during the OnLoad. If we see the same
key again (the same page is visited with the same roles) we grab the Control objects
from the HashTable, add them to the Control Tree. Then the Control Tree will
be turned into HTML in OnRender.
Cons: This would take some more memory than caching just the HTML;
it takes more CPU to Render the Controls every page view.
Pros: If the Creation of the Control Tree that represents the navigation
is expensive (more~ than ~50% of the totaly time it takes to fetch, build and render)
then it's an easy change to implement if you're already building the navigation with
HtmlControls in the code-behind.
2. Using the directive with the VaryByCustom parameter like this: .
Then, in the Global.asax you override GetVaryByCustomString, which will be automatically
called by the Pages. That's your opportunity to provide a KEY.
Not the HTML to cache, but rather the KEY by which to cache the rendered
HTML.
override public String GetVaryByCustomString(HttpContext
current, String arg)
{
switch(arg)
{
case "PageAndRolesKey": return GeneratePageAndRolesKey(current);
}
}
In this example, the GeneratePageAndRolesKey() function we'd look at the current page
and current roles and build a key like: "balances.aspx : Silver : Plan-A."
The rendered HTML is then stored in the Cache using the Key returned. If the
page is visited again, based on the key, the rendered HTML is retrieved from the Cache
and all the slow generation code is bypassed.
To help me visualize and conceptualize, I like to say that there is one instance of
a rendered header for each possible key.
Pros: Easy to type, easy to implement incorrectly. :)
Cons: Possibly hard to conceive of the cartesian explosion of
'flags.' It's always useful to write out the equation and a table of key combinations.
The OutputCache directive caches the entire UserControl (ASCX) so you can't just cache
a small portion of the UserControl. The UserControl is the 'atom.' You
CAN, however, have multiple UserControls and cache each differently. Be aware
though that that can cause another combinatoric explosion if you're not careful.