Increased performance for MOSS apps using the PortalSiteMapProvider

Performance is usually top-of-mind for any web site deployment, including MOSS. If you're writing code that deals with SharePoint items, and you're deploying that code to a site with even moderate traffic, you should consider using the PortalSiteMapProvider for increased performance. In this post, Chris Richard takes us through the feature and describes how it can be leveraged. Take it away Chris!

Introduction

The SharePoint OM works best when providing access to data in the "context" items - the web, page, list, list item, etc. that the request URL specified. Fetching the context web via SPContext.Current.Web and referencing the majority of the properties on that web completes quite quickly and without a high number of database roundtrips. However, there are a number of scenarios where a variety of data from multiple webs, lists and list items is required, and using the SharePoint OM to repeatedly fetch this data can become very costly. Navigation rendering is an example of such a scenario - rendering navigation on a particular page often involves fetching at least the title and description of a large number of ancestor and descendant webs.

MOSS employs a number of in-memory LRU caches to speed up access to such information while decreasing the load on the database server. Specific parts of this caching system are exposed through public caching APIs - CrossListQueryCache (https://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.publishing.crosslistquerycache.aspx), and CbqQueryCache (https://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.publishing.cbqquerycache.aspx) can be used to optimize repeated access to these types of queries which can sometimes be very expensive to run.

The cache also stores more basic SharePoint objects like SPWebs and SPListItems. The objects are fetched from the database when first requested and then stored in memory on the web server in order to speed up the processing of subsequent requests for the same data. This data stays in memory until memory pressure and usage patterns force it out, or the backing data is changed in the database. And although there isn't a public OM geared specifically to expose this functionality, the PortalSiteMapProvider exposes a number of methods that can be used to get at this cached data.

PortalSiteMapProvider Instances

In general, you shouldn't create new instances of PortalSiteMapProvider to be discarded after use in a single method. These objects contribute to a lot of memory use and are designed to be shared across a large number of requests. Any PortalSiteMapProvider instances declared in web.config can be accessed with:

 PortalSiteMapProvider portalProvider = (PortalSiteMapProvider)SiteMap.Providers["<InstanceName>"];

MOSS declares a number of instances of PortalSiteMapProvider in web.config which are used for rendering navigation. For convenience, the PSMP class exposes a number of static properties which allows even easier access to these standard named instances. In addition, there is a similar static property defined for "WebSiteMapProvider" which doesn't correspond to a definition in web.config, but is a useful instance of PortalSIteMapProvider for caching purposes as it returns only webs. By default, an instance of PSMP returns webs, Publishing pages, headings and authored links - the 4 different types of items that show up in navigation. The following properties are set on this provider at initialization time in order to remove (possibly) unwanted items:

 // do not limit the number of children a particular node may store
webSiteMapProvider.DynamicChildLimit = 0;
// do not include pages in results
webSiteMapProvider.IncludePages = IncludeOption.Never;
// do not include authored links in results
webSiteMapProvider.IncludeAuthoredLinks = false;
// do not include headings but return nodes beneath headings
webSiteMapProvider.FlattenHeadings = true;

See https://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.publishing.navigation.portalsitemapprovider_members.aspx for the full list of configurable properties for the provider. Most of these properties affect the collection of nodes returned by the GetChildNodes method which I'll touch on in just a minute. Depending on your scenario, it may make sense to define your own static instance of PortalSiteMapProvider with a configuration that suits your needs.

PortalSiteMapProvider.FindSiteMapNode

This method allows fast and easy access to webs by URL:

 PortalSiteMapProvider portalProvider = PortalSiteMapProvider.WebSiteMapProvider;
PortalSiteMapNode webNode = ((PortalSiteMapNode)portalProvider.FindSiteMapNode("/site/web"));
System.Console.WriteLine(webNode.Title);

You can also access Publishing pages in a similar fashion, using a different provider which doesn't exclude pages:

 PortalSiteMapProvider portalProvider = PortalSiteMapProvider.CurrentNavSiteMapProviderNoEncode;
PortalSiteMapNode pageNode = ((PortalSiteMapNode)portalProvider.FindSiteMapNode("/site/web/Pages/Page1.aspx"));
System.Console.WriteLine(pageNode.Title);

Be aware though, you cannot fetch the default page of a web, as you will be returned a node representing the web itself. This quirk has to do with providing the right set of navigation links for the navigation menus - after all, the PSMP is first and foremost a navigation site map provider. I discuss a different technique which does allow you to fetch the default page, a little later.

PortalSiteMapNode

All nodes returned by this provider are of type PortalSiteMapNode and may be safely cast to this type. The class exposes a number of properties, some of which are related to navigation specifically but most are generally useful. Title, Description and UniqueId are examples of simple but useful properties available on all nodes. The Type property provides information about the node's internal type, and may be used to determine if casting to a more specific type of PortalSiteMapNode is possible. WebNode is also a useful property, returning the web associated with a particular item; a node representing a web returns itself for its WebNode property. See https://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.publishing.navigation.portalsitemapnode_members.aspx for a list of all available properties.

PortalWebSiteMapNode

If a node has type NodeTypes.Area, it may be cast to a PortalWebSiteMapNode which provides a few extra methods for getting at cached data only available on webs. GetProperty and TryGetProperty are great for accessing properties found in the SPWeb.AllProperties property bag. See https://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.publishing.navigation.portalwebsitemapnode_members.aspx for a list of all members.

PortalListItemSiteMapNode

If a node has type NodeTypes.ListItem or NodeTypes.Page, it may be cast to a PortalListItemSiteMapNode. This type exposes indexers which allow access to cached field data by field name and ID. See https://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.publishing.navigation.portallistitemsitemapnode_members.aspx for a complete list of members.

PortalSiteMapProvider.GetChildNodes

This method has a number of overloads and allows fast and easy access to a particular node's navigation children (only webs and headings may be parents, sub-webs, Publishing pages, and the other navigation items show up as direct children). If you're using the "WebSiteMapProvider", GetChildNodes will only include webs in the child node collection. If you're not using a specially configured provider, but still want to control the type of children returned, you can use the following overload:

 PortalSiteMapProvider portalProvider = PortalSiteMapProvider.CurrentNavSiteMapProviderNoEncode;
SiteMapNodeCollection children = portalProvider.GetChildNodes(((PortalSiteMapNode)portalProvider.CurrentNode).WebNode,
    NodeTypes.Area, NodeTypes.Area);

The first NodeTypes parameter denotes the type of child nodes you want returned, the second denotes the types you want to have returned even if items of this type have been hidden from navigation. Specifying the same type(s) for both parameters will ensure that node of that type will be returned regardless of any navigation visibility settings. Multiple types may be specified as follows:

 PortalSiteMapProvider portalProvider = ortalSiteMapProvider.CurrentNavSiteMapProviderNoEncode
SiteMapNodeCollection children = portalProvider.GetChildNodes(((PortalSiteMapNode)portalProvider.CurrentNode).WebNode,
    NodeTypes.Area | NodeTypes.Page, NodeTypes.Area | NodeTypes.Page);

Note that the set of page nodes returned by this method will not include the page which is set to be the default page of the web. Continue reading for a more generic method of accessing cached list items that doesn't have this limitation.

PortalSiteMapProvider.GetCachedListItemsByQuery

List items which are Publishing pages are easily accessible via FindSiteMapNode and GetChildNodes, but for all other types of list items, this method allows access to cached list items by SPQuery. For example:

 SPQuery query = new SPQuery();
query.Query = query.Query = "<Neq><FieldRef Name=\"FSObjType\"></FieldRef><Value Type=\"Integer\">1</Value></Neq>";
SiteMapNodeCollection children = portalProvider.GetCachedListItemsByQuery(((PortalSiteMapNode)portalProvider.CurrentNode).WebNode,
    "Pages", query, SPContext.Current.Web)

This fetches all items from the Pages list, including the default page which GetChildNodes leaves out. The method uses the supplied context web to determine the current user's access rights and trim the results accordingly, and is not affected by any of the navigation-related configuration I mentioned earlier, so it doesn't really matter which instance of PSMP you call this on.

PortalSiteMapProvider.GetCachedList

This method provides access to cached lists, returning a node of type PortalListSiteMapNode. The data available via this interface is not extensive, but may meet your needs. See https://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.publishing.navigation.portallistsitemapnode_members.aspx for a complete list of members.

Drawbacks

Using PortalSiteMapProvider in this way can have some drawbacks. The first request for a particular web or page via FindSiteMapNode or GetChildNodes will take longer to fetch than a similar call into the WSS OM. This is because in this case the caching mechanism must roundtrip to the database and will often fetch more data than it needs in an attempt to pre-fetch and optimize subsequent requests. Therefore it is not useful to fetch data which is accessed very infrequently via these methods. The benefit associated does not show up until multiple calls have been made for the same data.

Also, using the above methods to fetch data which frequently changes can also be counterproductive. If the data changes frequently, it will be frequently invalidated and re-fetched from that database negating the benefits of caching. The invalidation mechanism is relatively coarse, with a change to a particular item invalidating any data in the same web as the item.

Finally, I should mention the "Site collection object cache" page, accessible via the "Site Settings" page. Here you can control a number of object cache properties - in particular the amount of memory, in MB, to allocate to the object cache on a per-site collection basis. If you notice sluggish performance on a large site collection, you might want to try upping the default limit of 100MB. Performance counters can help you fine tune cache performance by monitoring the effect of changing the cache size. The counters available on performance object "SharePoint Publishing Cache" will give you information about this caching system - be sure to select the instance that corresponds to your site collection URL.

--Chris Richard, ECM Developer