ASP.NET, Caching, and Cartesian Products
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:
n * x * 2y combinations of rendered headers
balances.aspx : Gold
balances.aspx : Silver
balances.aspx : Gold : Plan-A
balances.aspx : Silver : Plan-A
balances.aspx : Gold : Plan-B
balances.aspx : Silver : Plan-B
balances.aspx : Gold : Plan-A : Plan-B
balances.aspx : Silver : Plan-A : Plan-B
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 <%OutputCache%>directive with the VaryByCustom parameter like this: <%OutputCache VaryByParam="None" VaryByCustom="PageAndRolesKey" Duration="180" %>. 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)
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.