Site Map Providers

 

Introduction to the Provider Model
Membership Providers
Role Providers
Site Map Providers
Session State Providers
Profile Providers
Web Event Providers
Web Parts Personalization Providers
Custom Provider-Based Services
Hands-on Custom Providers: The Contoso Times

Site map providers provide the interface between ASP.NET's data-driven site-navigation features and site map data sources. The two most common reasons for writing a custom site map provider are:

  • You wish to store site maps in a data source that is not supported by the site map providers included with the .NET Framework, such as a Microsoft SQL Server database.
  • You wish to store site map data in an XML file whose schema differs from that of the one used by System.Web.XmlSiteMapProvider.

The fundamental job of a site map provider is to read site map data from a data source and build an upside-down tree of SiteMapNode objects (Figure 4-1), and to provide methods for retrieving nodes from the site map. Each SiteMapNode in the tree represents one node in the site map. SiteMapNode properties such as Title, Url, ParentNode, and ChildNodes define the characteristics of each node and allow the tree to be navigated up, down, and sideways. A single site map can be managed by one or several providers. Site map providers can form a tree of their own, linked together by their ParentProvider properties, with each provider in the tree claiming responsibility for a subset of the site map. A SiteMapNode's Provider property identifies the provider that "owns" that node.

Aa479033.sitemap_fig01s(en-us,MSDN.10).gif

Figure 1. Site map structure

ASP.NET assumes that each site map node's URL is unique with respect to other URLs in the site map. However, site maps do support nodes without URLs. Every SiteMapNode has a Key property that the provider initializes with a value that uniquely identifies the node. ASP.NET's XmlSiteMapProvider class sets SiteMapNode.Key equal to the node's URL if SiteMapNode.Url is neither null nor empty, or to a randomly generated GUID otherwise. Site map providers include methods for finding nodes by URL or by key.

Developers writing custom site map providers typically begin by deriving from System.Web.StaticSiteMapProvider, which derives from System.Web.SiteMapProvider. However, developers also have the option of deriving from SiteMapProvider directly. SiteMapProvider defines the basic contract between ASP.NET and site map providers. StaticSiteMapProvider goes much further, providing default implementations of most of SiteMapProvider's abstract methods and overriding key virtuals to provide functional, even optimized, implementations.

Deriving from StaticSiteMapProvider is appropriate for custom providers that read node data once (or infrequently) and then cache the information for the lifetime of the provider. Deriving from SiteMapProvider is appropriate for custom providers that query a database or other underlying data source in every method call.

The SiteMapProvider Class

System.Web.SiteMapProvider is prototyped as follows:

public abstract class SiteMapProvider : ProviderBase
{
    // Public events
    public event SiteMapResolveEventHandler SiteMapResolve;

    // Public properties
    public virtual SiteMapNode CurrentNode { get; }
    public bool EnableLocalization { get; set; }
    public virtual SiteMapProvider ParentProvider { get; set; }
    public string ResourceKey { get; set; }
    public virtual SiteMapProvider RootProvider { get; }
    public virtual SiteMapNode RootNode { get; }
    public bool SecurityTrimmingEnabled { get; }

    // Non-virtual methods
    protected SiteMapNode ResolveSiteMapNode(HttpContext context) {}
    protected static SiteMapNode GetRootNodeCoreFromProvider
        (SiteMapProvider provider) {}

    // Virtual methods
    public override void Initialize(string name,
        NameValueCollection attributes) {}
    protected virtual void AddNode(SiteMapNode node) {}
    protected internal virtual void AddNode(SiteMapNode node,
        SiteMapNode parentNode) {}
    protected internal virtual void RemoveNode(SiteMapNode node) {}
    public virtual SiteMapNode FindSiteMapNode(HttpContext context) {}
    public virtual SiteMapNode FindSiteMapNodeFromKey(string key) {}
    public virtual SiteMapNode GetCurrentNodeAndHintAncestorNodes
        (int upLevel) {}
    public virtual SiteMapNode GetCurrentNodeAndHintNeighborhoodNodes
        (int upLevel, int downLevel) {}
    public virtual SiteMapNode
        GetParentNodeRelativeToCurrentNodeAndHintDownFromParent
        (int walkupLevels, int relativeDepthFromWalkup) {}
    public virtual SiteMapNode
        GetParentNodeRelativeToNodeAndHintDownFromParent
        (SiteMapNode node, int walkupLevels,
        int relativeDepthFromWalkup) {}
    public virtual void HintAncestorNodes(SiteMapNode node,
        int upLevel) {}
    public virtual void HintNeighborhoodNodes(SiteMapNode node,
        int upLevel, int downLevel) {}
    public virtual bool IsAccessibleToUser(HttpContext context,
        SiteMapNode node) {}

    // Abstract methods
    public abstract GetChildNodes(SiteMapNode node);
    public abstract SiteMapNode FindSiteMapNode(string rawUrl);
    public abstract SiteMapNode GetParentNode(SiteMapNode node);
    protected internal abstract SiteMapNode GetRootNodeCore();
}

The following table describes SiteMapProvider's methods and properties and provides helpful notes regarding their implementation:

Method or Property Description
CurrentNode Returns a SiteMapNode reference to the site map node representing the current page (the page targeted by the current request). This property is read-only.
EnableLocalization Indicates whether localization is enabled for this provider. This property can be read or written.
ParentProvider Gets or sets a SiteMapProvider reference to this provider's parent provider, if any. Get accessor returns null (Nothing in Visual Basic) if this provider doesn't have a parent. This property can be read or written.
ResourceKey Gets or sets the provider's resource key-the root name of the resource files from which SiteMapNodes extract values for properties such as Title and Description. This property can be read or written.
RootProvider Returns a SiteMapProvider reference to the root site map provider. This property is read-only.
RootNode Returns a SiteMapNode reference to the site map's root node. This property is read-only.
SecurityTrimmingEnabled Returns a Boolean indicating whether security trimming is enabled. SiteMapProvider initializes this property from the securityTrimmingEnabled configuration attribute. This property is read-only.
Initialize Called to initialize the provider. The default implementation in SiteMapProvider initializes the provider's Description property, calls base.Initialize, and initializes the SecurityTrimmingEnabled property from the securityTrimmingEnabled configuration attribute.
AddNode (SiteMapNode) Adds a root SiteMapNode to the site map. The default implementation in SiteMapProvider calls AddNode (node, null), which throws a NotImplementedException.
AddNode (SiteMapNode, SiteMapNode) Adds a SiteMapNode to the site map as a child of the specified SiteMapNode, or as the root node if the specified SiteMapNode is null (Nothing in Visual Basic). The default implementation in SiteMapProvider throws a NotImplementedException, so this method should be overridden in a derived class.
RemoveNode Removes the specified SiteMapNode from the site map. The default implementation in SiteMapProvider throws a NotImplementedException, so this method should be overridden in a derived class.
FindSiteMapNode (HttpContext) Retrieves a SiteMapNode representing the current page. The default implementation in SiteMapProvider calls the abstract FindSiteMapNode method to return the SiteMapNode that corresponds to HttpContext.Request.RawUrl.
FindSiteMapNode (string) Returns a SiteMapNode representing the page at the specified URL. Returns null (Nothing in Visual Basic) if the specified node isn't found.

This method is abstract (MustOverride in Visual Basic) and must be overridden in a derived class.

FindSiteMapNodeFromKey Retrieves a SiteMapNode keyed by the specified key-that is, a SiteMapNode whose Key property matches the input key. Node lookups are normally performed based on URLs, but this method is provided so nodes that lack URLs can be retrieved from the site map. Returns null (Nothing in Visual Basic) if the specified node isn't found. The default implementation in SiteMapProvider always returns null, so this method should be overridden in a derived class.
GetCurrentNodeAnd-HintAncestorNodes Retrieves a SiteMapNode representing the current page. The default implementation in SiteMapProvider simply returns CurrentNode, but derived classes that don't store entire site maps in memory can override this method and return a SiteMapNode along with the number of generations of ancestor nodes specified in the upLevel parameter. Returns null (Nothing in Visual Basic) if the specified node isn't found.
GetCurrentNodeAnd-HintNeighborhoodNodes Retrieves a SiteMapNode representing the current page. The default implementation in SiteMapProvider simply returns CurrentNode, but derived classes that don't store entire site maps in memory can override this method and return a SiteMapNode along with the number of generations of ancestor nodes specified in the upLevel parameter and the number of generations of descendant nodes specified in the downLevel parameter. Returns null (Nothing in Visual Basic) if the specified node isn't found.
GetParentNodeRelativeToCurrent-NodeAndHintDownFromParent Retrieves a SiteMapNode representing an ancestor of the current node the specified number of generations higher in the hierarchy. Derived classes that don't store entire site maps in memory can override this method and return a SiteMapNode along with the number of generations of descendant nodes specified in the relativeDepthFromWalkup parameter. Returns null (Nothing in Visual Basic) if the specified node isn't found.
GetParentNodeRelativeToNodeAnd-HintDownFromParent Same as GetParentNodeRelativeToCurrentNodeAndHintDown-FromParent, but takes a SiteMapNode as input and performs lookup relative to that node rather than the current node.
HintAncestorNodes Takes the specified SiteMapNode and fills its ancestor hierarchy with the number of generations specified in the UpLevel parameter. The default implementation in SiteMapProvider does nothing.
HintNeighborhoodNodes Takes the specified SiteMapNode and fills its ancestor and descendant hierarchies with the number of generations specified in the UpLevel parameter and downLevel parameters. The default implementation in SiteMapProvider does nothing.
IsAccessibleToUser Returns a Boolean indicating whether the current user has permission to access the specified SiteMapNode. The default implementation in SiteMapProvider returns false if security trimming is enabled and the user does not belong to any of the roles associated with the SiteMapNode and the user does not have access to the corresponding URL according to any URL or file authorization rules currently in effect. SiteMapProvider also recognizes the role name "*" as an indication that everyone is permitted to access this node. Security trimming is described more fully in Security Trimming.
GetChildNodes Returns a SiteMapNodeCollection representing the specified SiteMapNode's children. Returns an empty SiteMapNodeCollection if the node has no children.

This method is abstract (MustOverride in Visual Basic) and must be overridden in a derived class.

GetParentNode Returns a SiteMapNode representing the specified SiteMapNode's parent. Returns null (in Visual Basic, Nothing) if the node has no parent.

This method is abstract (MustOverride in Visual Basic) and must be overridden in a derived class.

GetRootNodeCore Returns a SiteMapNode representing the root node of the site map managed by this provider. If this is the only site map provider, or if it is the top provider in a hierarchy of providers, GetRootNodeCore returns the same SiteMapNode as SiteMap.RootNode. Otherwise, it may return a SiteMapNode representing a node elsewhere in the site map hierarchy.

This method is abstract (MustOverride in Visual Basic) and must be overridden in a derived class.

Inside the ASP.NET Team
You may think it curious that in addition to supporting methods such as GetChildNodes and GetParent for retrieving nodes from a site map provider, the SiteMapProvider class also has "Hint" methods such as GetCurrentNodeAndHintAncestorNodes that can be called to say "give me a node and make sure that the hierarchy of nodes above and below it are populated to a specified depth." After all, isn't a SiteMapNode implicitly wired to its parent and child nodes without explicitly demanding that it be so? Here's some background from the ASP.NET team:

"We added the Hint* methods prior to Beta 1 after meeting with the Sharepoint and Content Management teams. Since for Sharepoint their custom site map providers run against a database, they wanted ways to be able to tell the provider that certain pieces of additional data above and beyond a specific requested node may be needed. Some of the methods incorporate both the hint and return a node (e.g., GetCurrentNodeAndHint-AncestorNodes), while others are just straight hint methods (e.g., HintAncestorNodes).""Either way, we would still want providers to implement methods such as GetChildNodes and GetParent to return nodes that can navigate into their children or up to their parent. A provider could just choose to return the set of relevant nodes based on the method that is called. Inside of SiteMapNode.Parent and SiteMapNode.ChildNodes, the provider is called to fetch more nodes. This is where the hint methods are usefula developer who understands the usage patterns on their site can intelligently hint the provider that parent or child nodes "around" a specific node may be requested, and thus a custom provider can proactively fetch and cache the information."

A site map provider that stores entire site maps in memory can simply use the default implementations of the "Hint" methods. Site map providers that don't store entire site maps in memory, however, might choose to override these methods and use them to optimize SiteMapNode lookups.

Security Trimming

The SiteMapProvider class contains built-in support for security trimming, which restricts the visibility of site map nodes based on users' role memberships. SiteMapProvider implements a Boolean read-only property named SecurityTrimmingEnabled, which indicates whether security trimming is enabled. Furthermore, SiteMapProvider.Initialize initializes this property from the provider's securityTrimmingEnabled configuration attribute. Internally, SiteMapProvider methods that retrieve nodes from the site map call the provider's virtual IsAccessibleToUser method to determine whether nodes can be retrieved. All a custom provider has to do to support security trimming is initialize each SiteMapNode's Roles property with an array of role names identifying users that are permitted to access that node, or "*" if everyone (including unauthenticated users and users who enjoy no role memberships) is permitted.

Security trimming doesn't necessarily prevent SiteMapProvider.IsAccessibleToUser from returning true if the user doesn't belong to one of the roles specified in a node's Roles property. Here's a synopsis of how IsAccessibleToUser uses SiteMapNode.Roles to authorize access to a node-that is, to determine whether to return true or false:

  1. If the current user is in a role specified in the node's Roles property, or if Roles is "*", the node is returned.
  2. If the current user is not in a role specified in the node's Roles property, then a URL authorization check is performed to determine whether the user has access to the node's URL. If the answer is yes, the node is returned.
  3. If Windows authentication is being used on the site, and if (1) and (2) failed, then a file authorization (ACL) check is performed against the node's URL using the current user's security token. If the ACL check succeeds, the node is returned.

Nodes lower in the hierarchy implicitly inherit the Roles properties of their parents, but only to a point. Refer to the sidebar below for a more detailed explanation.

Inside the ASP.NET Team
At first glance, it might appear that as a custom site map creates a tree of SiteMapNodes, it must explicitly flow the Roles properties of parents down to their children. However, that's not the case. Here's an explanation from the team:

"In terms of role inheritance-there is not direct inheritance. Instead, when you iterate through a set of nodes, if you encounter a node that the current user is not authorized to access, you should stop. As an example, suppose some piece of code is recursing down through the nodes, and when it reaches a certain node, IsAccessibleToUser returns false. The code should stop iterating at that point and go no lower. However, if you were to call FindSiteMapNode to get one of the child nodes directly, that would work."

In other words, a provider need not copy a parent node's Roles property to child nodes. Nor is FindSiteMapNode or IsAccessibleToUser obligated to walk up the SiteMapNode tree to determine whether a specified node is accessible. Node accessibility is primarily a mechanism allowing controls such as TreeViews and Menus, which iterate through trees of SiteMapNodes from top to bottom in order to render site maps into HTML, to stop iterating when they encounter a node that the current user lacks permission to view.

Content Localization

The combination of SiteMapProvider's EnableLocalization and ResourceKey properties and SiteMapNode's ResourceKey property enables site map providers to support content localization. If localization is enabled (as indicated by the provider's EnableLocalization property), a provider may use the ResourceKey properties to load text for SiteMapNode's Title and Description properties, and even for custom properties, from resources (for example, compiled RESX files).

Resource keys can be implicit or explicit. Here's an example of an XmlSiteMapProvider-compatible site map that uses implicit resource keys to load node titles and descriptions:

<siteMap enableLocalization="true">
  <siteMapNode description="Home" url="~/Default.aspx">
    <siteMapNode title="Autos" description="Autos"
      url="~/Autos.aspx" resourceKey="Autos" />
    <siteMapNode title="Games" description="Games"
      url="~/Games.aspx" resourceKey="Games" />
    <siteMapNode title="Health" description="Health"
      url="~/Health.aspx" resourceKey="Health" />
    <siteMapNode title="News" description="News"
      url="~/News.aspx" resourceKey="News" />
  </siteMapNode>
</siteMap>

In this example, the "Autos" site map node's Title and Description properties are taken from resources named Autos.Title and Autos.Description (and default to "Autos" and "Autos" if those resources don't exist or localization isn't enabled). SiteMapNode formulates the resource names by combining resourceKey values and property names; it also handles the chore of loading the resources.

The root name of the file containing the resources is specified using the provider's ResourceKey property. For example, suppose ResourceKey="SiteMapResources", and that localization resources are defined in RESX files deployed in the application's App_GlobalResources directory for automatic compilation. SiteMapNode will therefore extract resources from SiteMapResources.culture.resx, where culture is the culture expressed in Thread.CurrentThread.CurrentUICulture. Resources for the fr culture would come from SiteMapResources.fr.resx, resources for en-us would come from SiteMapResources.en-us.resx, and so on. Requests lacking a culture specifier would default to SiteMapResources.resx.

A site map provider that supports localization via implicit resource keys should do the following:

  • Initialize its ResourceKey property with a root resource file name. This value could come from a custom configuration attribute, or it could be based on something else entirely. ASP.NET's XmlSiteMapProvider, for example, sets the provider's ResourceKey property equal to the site map file name specified with the siteMapFile configuration attribute (which defaults to Web.sitemap).
  • Pass resource key values specified for site map nodes—for example, the resourceKey attribute values in <siteMapNode> elements—to SiteMapNode's constructor as the implicitResourceKey parameter.
  • Set its own EnableLocalization property to true.

Although SiteMapProvider implements the EnableLocalization property, neither SiteMapProvider nor StaticSiteMapProvider initializes that property from a configuration attribute. If you want to support enableLocalization in your provider's configuration, you should do as XmlSiteMapProvider does and initialize the provider's EnableLocalization property from the enableLocalization attribute of the <siteMap> element in the site map file. It's important to set the provider's EnableLocalization property to true if you wish to localize site map nodes, because the SiteMapNode class checks that property before deciding whether to load content from resources.

The StaticSiteMapProvider Class

Whereas SiteMapProvider defines the basic contract between ASP.NET and site map providers, StaticSiteMapProvider aids developers in implementing that contract. StaticSiteMapProvider is the base class for ASP.NET's XmlSiteMapProvider. It can also be used as the base class for custom site map providers. Provider classes that derive from StaticSiteMapProvider require considerably less code than providers derived from SiteMapProvider.

The word "Static" in StaticSiteMapProvider refers to the fact that the site map data source is static. Nonetheless, while the data source may be static, the site map itself does not have to be. Developers can add, remove, and change site map nodes on the fly by responding to a site map provider's SiteMapResolve events.

Inside the ASP.NET Team
ASP.NET's XmlSiteMapProvider goes to the extra trouble of monitoring the site map file and reloading it if it changes. While not required of a custom site map provider, that behavior will certainly be appreciated by administrators who want to modify a site map without having to restart the application. The System.IO.FileSystemWatcher class provides an efficient means for monitoring files for changes. If you use it, don't forget to implement IDisposable in the derived class and close the FileSystemWatcher in the Dispose method. If site map data is stored in a Microsoft SQL Server database, consider using ASP.NET 2.0's SqlCacheDependency class to monitor the database for changes.

System.Web.StaticSiteMapProvider is prototyped as follows:

public abstract class StaticSiteMapProvider : SiteMapProvider
{
    public abstract SiteMapNode BuildSiteMap();
    protected virtual void Clear() {}
    protected internal override void AddNode(SiteMapNode node,
        SiteMapNode parentNode) {}
    protected internal override void RemoveNode(SiteMapNode node) {}
    public override SiteMapNode FindSiteMapNode(string rawUrl) {}
    public override SiteMapNode FindSiteMapNodeFromKey(string key) {}
    public override SiteMapNodeCollection
        GetChildNodes(SiteMapNode node) {}
    public override SiteMapNode GetParentNode(SiteMapNode node) {}
}

The following table describes StaticSiteMapProvider's methods and provides helpful notes regarding their implementation:

Method Description
BuildSiteMap Called to read the site map from the data source and return a reference to the root SiteMapNode. Since this method may be called more than once by ASP.NET, the method implementation should include an internal check that refrains from reloading the site map if it has already been loaded.

This method is abstract (MustOverride in Visual Basic) and must be overridden in a derived class.

Clear Clears the site map by removing all SiteMapNodes.
FindSiteMapNode Returns a SiteMapNode representing the page at the specified URL. Returns null (Nothing in Visual Basic) if the specified node isn't found.
AddNode Adds a SiteMapNode to the site map as a child of the specified SiteMapNode, or as the root node if the specified SiteMapNode is null (Nothing in Visual Basic). The default implementation in StaticSiteMapProvider performs several important checks on the node before adding it to the site map, including a check for duplicate Url or Key properties.
RemoveNode Removes the specified SiteMapNode from the site map.
FindSiteMapNodeFromKey Retrieves a SiteMapNode keyed by the specified key—that is, a SiteMapNode whose Key property matches the input key. Node lookups are normally performed based on URLs, but this method is provided so nodes that lack URLs can be retrieved from the site map. Returns null (Nothing in Visual Basic) if the specified node isn't found.
GetChildNodes Returns a SiteMapNodeCollection representing the specified SiteMapNode's children. Returns an empty SiteMapNodeCollection if the node has no children.
GetParentNode Returns a SiteMapNode representing the specified SiteMapNode's parent. Returns null (Nothing in Visual Basic) if the node has no parent.

StaticSiteMapProvider provides default implementations of most of SiteMapProvider's abstract methods (the notable exception being GetRootNodeCore), and internally it uses a set of Hashtables to make lookups performed by FindSiteMapNode, GetParentNode, and GetChildNodes fast and efficient. It also defines an abstract method of its own, BuildSiteMap, which signals the provider to read the data source and build the site map. BuildSiteMap can be (and in practice, is) called many times throughout the provider's lifetime, so it's crucial to build in an internal check to make sure the site map is loaded just once. (Subsequent calls to BuildSiteMap can simply return a reference to the existing root node.) In addition, BuildSiteMap should generally not call other site map provider methods or properties, because many of the default implementations of those methods and properties call BuildSiteMap. For example, the simple act of reading RootNode in BuildSiteMap causes a recursive condition that terminates in a stack overflow.

SqlSiteMapProvider

SqlSiteMapProvider is a StaticSiteMapProvider-derivative that demonstrates the key ingredients that go into a custom site map provider. Unlike XmlSiteMapProvider, which reads site map data from an XML file, SqlSiteMapProvider reads site maps from a SQL Server database. It doesn't support localization, but it does support other significant features found in XmlSiteMapProvider, including security trimming and site maps of unlimited depth. SqlSiteMapProvider's source code appears below.

SqlSiteMapProvider

using System;
using System.Web;
using System.Data.SqlClient;
using System.Collections.Specialized;
using System.Configuration;
using System.Web.Configuration;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Configuration.Provider;
using System.Security.Permissions;
using System.Data.Common;

[SqlClientPermission (SecurityAction.Demand, Unrestricted=true)]
public class SqlSiteMapProvider : StaticSiteMapProvider
{
    private const string _errmsg1 = "Missing node ID";
    private const string _errmsg2 = "Duplicate node ID";
    private const string _errmsg3 = "Missing parent ID";
    private const string _errmsg4 = "Invalid parent ID";
    private const string _errmsg5 =
        "Empty or missing connectionStringName";
    private const string _errmsg6 = "Missing connection string";
    private const string _errmsg7 = "Empty connection string";

    private string _connect;
    private int _indexID, _indexTitle, _indexUrl,
        _indexDesc, _indexRoles, _indexParent;
    private Dictionary<int, SiteMapNode> _nodes =
        new Dictionary<int, SiteMapNode>(16);
    private SiteMapNode _root;

    public override void Initialize (string name,
        NameValueCollection config)
    {
        // Verify that config isn't null
        if (config == null)
            throw new ArgumentNullException("config");

        // Assign the provider a default name if it doesn't have one
        if (String.IsNullOrEmpty(name))
            name = "SqlSiteMapProvider";

        // Add a default "description" attribute to config if the
        // attribute doesn't exist or is empty
        if (string.IsNullOrEmpty(config["description"]))
        {
            config.Remove("description");
            config.Add("description", "SQL site map provider");
        }

        // Call the base class's Initialize method
        base.Initialize(name, config);

        // Initialize _connect
        string connect = config["connectionStringName"];

        if (String.IsNullOrEmpty (connect))
            throw new ProviderException (_errmsg5);

        config.Remove ("connectionStringName");

        if (WebConfigurationManager.ConnectionStrings[connect] == null)
            throw new ProviderException (_errmsg6);

        _connect = WebConfigurationManager.ConnectionStrings
            [connect].ConnectionString;

        if (String.IsNullOrEmpty (_connect))
            throw new ProviderException (_errmsg7);
        
        // In beta 2, SiteMapProvider processes the
        // securityTrimmingEnabled attribute but fails to remove it.
        // Remove it now so we can check for unrecognized
        // configuration attributes.

        if (config["securityTrimmingEnabled"] != null)
            config.Remove("securityTrimmingEnabled");
        
        // Throw an exception if unrecognized attributes remain
        if (config.Count > 0)
        {
            string attr = config.GetKey(0);
            if (!String.IsNullOrEmpty(attr))
                throw new ProviderException
                    ("Unrecognized attribute: " + attr);
        }
    }

   public override SiteMapNode BuildSiteMap()
   {
        lock (this)
        {
            // Return immediately if this method has been called before
            if (_root != null)
                return _root;

            // Query the database for site map nodes
            SqlConnection connection = new SqlConnection(_connect);
            
            try
            {
                connection.Open();
                SqlCommand command =
                    new SqlCommand("proc_GetSiteMap", connection);
                command.CommandType = CommandType.StoredProcedure;            
                SqlDataReader reader = command.ExecuteReader();
                _indexID = reader.GetOrdinal("ID");
                _indexUrl = reader.GetOrdinal("Url");
                _indexTitle = reader.GetOrdinal("Title");
                _indexDesc = reader.GetOrdinal("Description");
                _indexRoles = reader.GetOrdinal("Roles");
                _indexParent = reader.GetOrdinal("Parent");

                if (reader.Read())
                {
                    // Create the root SiteMapNode and add it to
                    // the site map
                    _root = CreateSiteMapNodeFromDataReader(reader);
                    AddNode(_root, null);

                    // Build a tree of SiteMapNodes underneath
                    // the root node
                    while (reader.Read())
                    {
                        // Create another site map node and
                        // add it to the site map
                        SiteMapNode node =
                            CreateSiteMapNodeFromDataReader(reader);
                        AddNode(node,
                            GetParentNodeFromDataReader (reader));
                    }
                }
            }
            finally
            {
                connection.Close();
            }

            // Return the root SiteMapNode
            return _root;
        }
    }

    protected override SiteMapNode GetRootNodeCore ()
    {
        BuildSiteMap ();
        return _root;
    }

    // Helper methods
    private SiteMapNode
        CreateSiteMapNodeFromDataReader (DbDataReader reader)
    {
        // Make sure the node ID is present
        if (reader.IsDBNull (_indexID))
            throw new ProviderException (_errmsg1);

        // Get the node ID from the DataReader
        int id = reader.GetInt32 (_indexID);

        // Make sure the node ID is unique
        if (_nodes.ContainsKey(id))
            throw new ProviderException(_errmsg2);

        // Get title, URL, description, and roles from the DataReader
        string title = reader.IsDBNull (_indexTitle) ?
            null : reader.GetString (_indexTitle).Trim ();
        string url = reader.IsDBNull (_indexUrl) ?
            null : reader.GetString (_indexUrl).Trim ();
        string description = reader.IsDBNull (_indexDesc) ?
            null : reader.GetString (_indexDesc).Trim ();
        string roles = reader.IsDBNull(_indexRoles) ?
            null : reader.GetString(_indexRoles).Trim();

        // If roles were specified, turn the list into a string array
        string[] rolelist = null;
        if (!String.IsNullOrEmpty(roles))
            rolelist = roles.Split(new char[] { ',', ';' }, 512);

        // Create a SiteMapNode
        SiteMapNode node = new SiteMapNode(this, id.ToString(), url,
            title, description, rolelist, null, null, null);

        // Record the node in the _nodes dictionary
        _nodes.Add(id, node);
       
        // Return the node        
        return node;        
    }

    private SiteMapNode
        GetParentNodeFromDataReader(DbDataReader reader)
    {
        // Make sure the parent ID is present
        if (reader.IsDBNull (_indexParent))
            throw new ProviderException (_errmsg3);

        // Get the parent ID from the DataReader
        int pid = reader.GetInt32(_indexParent);

        // Make sure the parent ID is valid
        if (!_nodes.ContainsKey(pid))
            throw new ProviderException(_errmsg4);

        // Return the parent SiteMapNode
        return _nodes[pid];
    }
}

The heart of SqlSiteMapProvider is its BuildSiteMap method, which queries the database for site map node data and constructs SiteMapNodes from the query results. The query is performed by calling the stored procedure named proc_GetSiteMap, which is defined as follows:

CREATE PROCEDURE proc_GetSiteMap AS SELECT [ID], [Title],
[Description], [Url], [Roles], [Parent] FROM [SiteMap] ORDER BY [ID]

The helper method CreateSiteMapNodeFromDataReader does the node construction, checking for errors such as missing or non-unique node IDs as well. CreateSiteMapNodeFromDataReader also records each SiteMapNode that it creates in an internal dictionary used by the other helper method, GetParentNodeFromDataReader, to retrieve a reference to a node's parent before adding the node to the site map with AddNode.

SqlSiteMapProvider inherits support for the securityTrimmingEnabled configuration attribute from SiteMapProvider. It also supports one configuration attribute of its own: connectionStringName. The provider's Initialize method reads connectionStringName and BuildSiteMap uses it to read a database connection string from the <connectionStrings> configuration section. This is the connection string used to connect to the SQL Server database containing the site map. The Web.config file below makes SqlSiteMapProvider the default provider and provides it with a connection string.

Web.config file making SqlSiteMapProvider the default site map provider and enabling security trimming

<configuration>
  <connectionStrings>
    <add name="SiteMapConnectionString" connectionString="..." />
  </connectionStrings>
  <system.web>
    <siteMap enabled="true" defaultProvider="AspNetSqlSiteMapProvider">
      <providers>
        <add name="AspNetSqlSiteMapProvider"
          type="SqlSiteMapProvider, CustomProviders"
          description="SQL Server site map provider"
          securityTrimmingEnabled="true"
          connectionStringName="SiteMapConnectionString"
        />
      </providers>
    </siteMap>
  </system.web>
</configuration>

The SQL script below creates a SqlSiteMapProvider-compatible SiteMap table. The Title, Description, Url, and Roles columns correspond to the SiteMapNode properties of the same names. The ID and Parent columns serve to uniquely identify nodes in the site map and form parent-child relationships between them. Every node must have a unique ID in the ID column, and every node except the root node must contain an ID in the Parent column identifying the node's parent. The one constraint to be aware of is that a child node's ID must be greater than its parent's ID. In other words, a node with an ID of 100 can be the child of a node with an ID of 99, but it can't be the child of a node with an ID of 101. That's a consequence of the manner in which SqlSiteMapProvider builds the site map in memory as it reads nodes from the database.

SQL script for creating a SqlSiteMapProvider-compatible SiteMap table

CREATE TABLE [dbo].[SiteMap] (
    [ID]          [int] NOT NULL,
    [Title]       [varchar] (32),
    [Description] [varchar] (512),
    [Url]         [varchar] (512),
    [Roles]       [varchar] (512),
    [Parent]      [int]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[SiteMap] ADD 
    CONSTRAINT [PK_SiteMap] PRIMARY KEY CLUSTERED 
    (
        [ID]
    )  ON [PRIMARY] 
GO

Figure 2 shows a view in SQL Server Enterprise Manager of a SiteMap table. Observe that each node has a unique ID and a unique URL. (Tthe latter is a requirement of site maps managed by site map providers that derive from StaticSiteMapProvider.) All nodes but the root node also have a parent ID that refers to another node in the table, and the parent's ID is always less than the child's ID. Finally, because the "Entertainment" node has a Roles value of "Members,Administrators," a navigational control such as a TreeView or Menu will only render that node and its children if the user viewing the page has been authenticated and is a member of the Members or Administrators group. That's assuming, of course, that security trimming is enabled. If security trimming is not enabled, all nodes are visible to all users. SqlSiteMapProvider contains no special code to make security trimming work; that logic is inherited from SiteMapProvider and StaticSiteMapProvider.

Click here for larger image.

Figure 2. Sample SiteMap table (Click on the graphic for a larger image)

Click here to continue on to part 4, Session State Providers

© Microsoft Corporation. All rights reserved.