Shining Treasures Little gems of code, web development, and search engine marketing

How to Make MvcSiteMapProvider Remember a User’s Position

Today I am going to talk about something that many people who are new to MvcSiteMapProvider struggle with – making the breadcrumb trail remember the last position the user was at – including the ID.

In short, you can’t do this. MvcSiteMapProvider doesn’t remember a user’s position at all – that simply isn’t how it works. In fact, there is no usage of session state, cookies, or anything user related that can remember a user’s position. Everything that MvcSiteMapProvider does is site wide. It is a site wide map, or sitemap that can tell where the current request is by looking it up in the map. This makes it useful for both end users and for search engines.

Note: Be sure to check out the code download at the end of this post for working examples.

How it Works

MvcSiteMapProvider can be used to create a breadcrumb trail to make it appear to remember where the user is by using their unique position within the map to show the current node and the other nodes relative to it.

The Map

First, we configure a map of nodes. Each node represents either a single unique combination of route values or a single unique URL. Most commonly the map is constructed in an XML file named Mvc.sitemap, but we can also build this map from other sources such as .NET attributes or database data. This map is loaded into the provider and then cached. The cache is shared between every user of the site.

The Match

After the site map is built, each HTTP request that comes in is matched against the nodes until a pair is found. This matching process is what makes MvcSiteMapProvider work with ASP.NET MVC because it can match either a route (including the action parameters) or a URL.

image

Once a match is made (the “current” node), the SiteMapPath HTML helper can walk up the node tree to build what you see in the breadcrumb trail – it is all based on what is configured in the map.

Home  >  About  >  About Me

The principle is based on the fact that the route only matches a single node in the tree. If your route matches more than one node, you will only ever see the first node that is matched and all of the additional matches will be ignored.

It’s important to understand that MvcSiteMapProvider doesn’t match a route or a URL to an action method or other resource – that is MVC’s job. Our task is to match the route or URL with a node in the site map, and it ends there.

How it Works with Controller Action Method Parameters

Now that we have covered the basic principle of how it works, let’s see what happens when we throw an action method parameter into the mix. Keep in mind, an action method parameter can have multiple values (id  = 1, id = 2, etc.).

The Route

For the sake of this example, let’s just say we are using the default route that comes with the MVC template, and we will use the “id” as the name of our action method parameter. Here is the route for your reference:

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Basically, we have 3 parameters – a “controller”, and “action”, and an “id”. The id is optional – we don’t have to use it if we don’t want.

For any newbie’s out there who don’t know this, if you add additional values to the RouteValues collection, when building a URL MVC will append the values as a querystring values instead of to the path. This is because the parameter isn’t in the actual route configuration. If the name of the parameter is “id” in the route configuration, and in your sitemap you add “myId”, you will get a URL that looks something like this “/Home/About?myId=2”. If that is undesired, note that the URL will only be built correctly if your routes are set up correctly.

The Controller Action Method

For this example, we are going to make a basic products controller with an action method for displaying products. Here is what our controller class looks like.

public class ProductsController : Controller
{
    //
    // GET: /Products/

    public ActionResult Index()
    {
        return View();
    }

    //
    // GET: /Products/Details/5

    public ActionResult Details(int id)
    {
        ViewBag.id = id;
        return View();
    }
}

The Match

This is where a lot of people get tripped up. In order for the matching to work, there usually should be a single node for it to match. This can be accomplished by adding a node for every potential “id” that it could match.

First of all, let’s see what this means to us in a visual sense.

image

As you can see, the route is matching the “AA Battery” node because the “id” is the same as the “id” in the route of the current HTTP request, and the other values match as well. For the match to work, the exact same parameters must be on both sides of the equation. If we were to configure this same example in MvcSiteMapProvider and view it using the SiteMapPath HTML helper, the result would look like this:

Home  >  Products  >  AA Battery

You have probably already guessed what will happen if another HTTP request comes in with an “id” of 2. It will match the “Raincoat” node instead.

image

In that case, the SiteMapPath would look like:

Home  >  Products  >  Raincoat

Now, let’s expand this example a bit. Let’s say that our product page links to a page showing products related to the current product.

image

Our current HTTP request is for the related products page for AA Battery. In this case, our SiteMapPath might look like this:

Home  >  Products  >  AA Battery  >  Related

But where the real magic happens is not during the current request, but during the subsequent action by the same user. For example, if the user decided to click on the “AA Battery” link, they would be back at the “AA Battery” page. Not because the sitemap remembered where the user was, but because the “AA Battery” node itself is tracking the “id” value of 1 for every user of the web site.

It’s important to recognize that for “AA Battery” and “Related: AA Battery”, even if the route value names were not both “id” and the values of the route value were not the same that MvcSiteMapProvider will still be able to “remember” the position of the “parent” node based on its relative position in the map. This makes it possible to nest the nodes for several levels below the one that is tracking the “id” and it will never “forget” what the URL is behind that node.

Tip: In most cases, you will want to add a node for every possible “id” in the sitemap. The primary reason for this is that it will allow the sitemap to track navigation to every page and also allow all of the pages to be in your /sitemap.xml end point that you submit to search engines for indexing. You can save yourself a lot of hair (from all of the unnecessary head scratching) if you do it this way. All of the nodes will be cached for all users on your site, so this is very efficient. If you need to dynamically add the “id”s to the sitemap from an external data source, you can use a DynamicNodeProvider (simple) or build your own SiteMapBuilder (advanced). For extremely large sites, you can even extend the cache if necessary.

Forcing a Match

There are times when we want to force every “id” to match a specific node. This can come in handy when your main navigation is based on a list or table of database data and you only want to create a SiteMapPath (breadcrumb trail) to accompany it. A good example of this is a set of administration pages to edit data and you don’t know ahead of time what the potential “id” will be (i.e. CRUD operations). These pages typically won’t be indexed by search engines, so there is little point in adding every id as a node to the sitemap. We just need a single node for each of these administration pages so the SiteMapPath HTML helper will function.  And, again – the main navigation is based on a table that is created from the database data directly. Then we just need to add a preserved route parameter value to force all “id”s to match that one node.

It is important to note that the Menu HTML helper, the SiteMap HTML helper, and the /sitemap.xml endpoint will not function correctly when using this method. If you want those to work, you must add every item as a node to the sitemap. If the nodes are based on data from an external source, you can populate them using DynamicNodeProviders.

By default, the match won’t occur if you just add a node with a controller and action, even though in the route the “id” is optional. As you can see in the diagram, it is because we are missing the “id” from one side of the equation.

image

In order to force a match, we need to balance the equation. That is, we need to add an “id” with a value of “2” to the “Edit”node of the sitemap. There are two different ways to achieve this.

  1. Add the parameter name to the PreservedRouteParameters collection.
  2. Add the parameter name and value to the RouteValues dictionary.

There is a third option – remove the “id” from the HTTP request. But there could be any number of things dependent on that value. Clearly, that is a road you don’t want to go down because it will wreak havoc on the rest of your application.

The PreservedRouteParameters Collection

A preserved route parameter is a value that is copied (or preserved) from the current HTTP request to a specific node. The values are copied over based on a name or names that are configured.

image

Note that after the current HTTP request is finished the value that is preserved will be discarded. It will not be remembered for the next request the user makes.

This copy operation is handled automatically by MvcSiteMapProvider when configured in the XML as follows.

<mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="Products" controller="Product" action="Index">
        <mvcSiteMapNode title="Details" controller="Product" action="Details" preservedRouteParameters="id">
            <mvcSiteMapNode title="Edit" controller="Product" action="Edit" preservedRouteParameters="id"/>
        </mvcSiteMapNode>
    </mvcSiteMapNode>
</mvcSiteMapNode>

If we wanted to add more than one preservedRouteParameter, we would separate them with a comma, like this: “id,anotherId” and each name in the list will have its value automatically copied before attempting to match the node. Note that they will only be copied over to the node where the preservedRouteParameters value is configured – all other nodes will not consider this in the comparison.

Note that we are also preserving the parameter on the parent “Details” node in this case, because we want it to track the id from the current request as well. This allows the navigation back to the details node for the same product as the edit node.

Home  >  Products  >  Details  >  Edit

Above is what the breadcrumb trail looks like when the user is on the Edit page. If the user clicks the Details node from that page, they will see the details for the product with the same “id” as the edit node. That is because the preserved route parameter is being copied from the current request to both the details and edit nodes. After this copy process is completed, the URLs are resolved when the view is processed, which makes the URL include the value for navigation.

It is also possible to set the preserved route parameters collection using the MvcSiteMapNodeAttribute on the action method. Note that all we are doing here is defining the node as an attribute rather than in the XML file, so after the node is loaded into the sitemap it works exactly the same as the XML example above.

[MvcSiteMapNode(Title = "Edit", Key = "Edit", PreservedRouteParameters = "id")]
public ActionResult Edit(int id)
{
    ViewBag.id = id;

    // Implementation omitted...

    return View();
}

Note: It should be obvious that you can only use this method (or the one in the next topic) if your child node (current node) contains all of the action parameter values for all of its ancestor (parent, grandparent, etc.) nodes. That is because it never remembers anything from one request to the next and the current request requires this information to build its ancestor URLs for the breadcrumb trail. If you find yourself in this situation, I urge you to consider dynamic node providers and 1-to-1 node matching as your primary design choice. You can also address this by changing your routes so they contain all of the parameter information of all ancestor nodes, but that isn’t always practical. If you are thinking about saving the parameter values into session state or a cookie and writing them back to the node on each request, keep in mind that your breadcrumb trail will fall apart if your user navigates directly to your child node without first visiting the ancestor nodes that require the parameters.

The RouteValues Dictionary

The RouteValues dictionary is the place where the sitemap node stores the controller, action, area, and any “unknown” parameters that are defined in the XML. This dictionary is what is used to actually make the match comparison between the node and the current request. It shouldn’t come as a surprise that editing these values directly can be used to force a match (or even force a non-match).

As a side note, the dictionary is request-safe. The sitemap tree is stored in a single cache that is shared between all users. However, if you edit the RouteValues property (which is a RouteValues dictionary), it will automatically make a request-level copy and store your edits (whether they are additions, updates, or deletions). As soon as the request is terminated, these edits will be gone and the next request will revert back to what is in the shared cache. In a nutshell, you don’t have to worry about making changes that will affect the shared cache, because your changes will only affect the current request.

Now that we know how the preserved route parameters work, here is an example of copying the id from the current request inside of a controller action method. This is equivalent to what was configured in XML above.

public ActionResult Edit(int id)
{
    var node = SiteMaps.Current.FindSiteMapNodeFromKey("Product_Edit");
    if (node != null)
    {
        node.RouteValues["id"] = id;
        var parent = node.ParentNode;
        if (parent != null)
        {
            parent.RouteValues["id"] = id;
        }
    }

    // Implementation omitted
}

All we are doing is copying the id parameter that is passed into our action method from the current request into the Edit node and its parent node. This code assumes you added a key property to the Edit node with the value “Product_Edit”.

<mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="Products" controller="Product" action="Index">
        <mvcSiteMapNode title="Details" controller="Product" action="Details">
            <mvcSiteMapNode title="Edit" controller="Product" action="Edit" key="Product_Edit"/>
        </mvcSiteMapNode>
    </mvcSiteMapNode>
</mvcSiteMapNode>

If you add this configuration to a project and run it, you will probably notice that it doesn’t work. The reason for this is because by default MvcSiteMapProvider caches the URLs automatically when it builds the SiteMap. This caching can be disabled either at the node level or it can be disabled at the SiteMap level. As of version 4.2.0, you can also disable it when using internal DI via web.config.

Disabling URL Resolution Caching

To disable URL resolution caching at the node level, add cacheResolvedUrl=”false” to your configuration.

<mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="Products" controller="Product" action="Index">
        <mvcSiteMapNode title="Details" controller="Product" action="Details" cacheResolvedUrl="false">
            <mvcSiteMapNode title="Edit" controller="Product" action="Edit" cacheResolvedUrl="false" key="Product_Edit"/>
        </mvcSiteMapNode>
    </mvcSiteMapNode>
</mvcSiteMapNode>

You only need to disable caching on URLs that you want to change per request. If your URLs contain user data, you should disable caching on the nodes that represent those URLs or consider disabling URL resolution caching site-wide.

If you add at least 1 preservedRouteParameter to the configuration for the node, the cache will automatically disable for the node – that is why it worked in the case above.

To disable URL caching on the entire SiteMap, edit the DI configuration of the SiteMapBuilderSet to exclude the VisitingSiteMapBuilder from being included. Here is how you would do that in StructureMap.

// Before
var builder = this.For<ISiteMapBuilder>().Use<CompositeSiteMapBuilder>()
    .EnumerableOf<ISiteMapBuilder>().Contains(y =>
    {
        y.Type<XmlSiteMapBuilder>()
            .Ctor<ISiteMapXmlReservedAttributeNameProvider>().Is(reservedAttributeNameProvider)
            .Ctor<IXmlSource>().Is(xmlSource);
        y.Type<ReflectionSiteMapBuilder>()
            .Ctor<IEnumerable<string>>("includeAssemblies").Is(includeAssembliesForScan)
            .Ctor<IEnumerable<string>>("excludeAssemblies").Is(new string[0]);
        y.Type<VisitingSiteMapBuilder>();
    });

// After
var builder = this.For<ISiteMapBuilder>().Use<CompositeSiteMapBuilder>()
    .EnumerableOf<ISiteMapBuilder>().Contains(y =>
    {
        y.Type<XmlSiteMapBuilder>()
            .Ctor<ISiteMapXmlReservedAttributeNameProvider>().Is(reservedAttributeNameProvider)
            .Ctor<IXmlSource>().Is(xmlSource);
        y.Type<ReflectionSiteMapBuilder>()
            .Ctor<IEnumerable<string>>("includeAssemblies").Is(includeAssembliesForScan)
            .Ctor<IEnumerable<string>>("excludeAssemblies").Is(new string[0]);
    });

This removes all of the visitors. Technically, we only need to remove the UrlResolvingSiteMapNodeVisitor. Since this is the only built-in visitor, this configuration is okay unless you have built your own custom visitors, in which case you will have to ensure only UrlResolvingSiteMapNodeVisitor is removed.

Finally, if you are using internal DI, you can simply set the MvcSiteMapProvider_EnableResolvedUrlCaching to false in web.config (v4.2.0 and higher).

<appSettings>
    <add key="MvcSiteMapProvider_EnableResolvedUrlCaching" value="false"/>
</appSettings>

So, to review – disable URL resolution caching on your node URLs if you want to overwrite them per request. Also, always disable URL resolution caching on node URLs that contain user or session specific data. If you prefer not to do surgery, you can disable URL resolution caching at the SiteMap level.

Fixing the Title

Now that we have our nodes working and building the correct URLs, we need to set the text of the links. If you have built an application using the above code, your breadcrumb trail now looks like the following when you navigate to /Product/Edit/2.

Home  >  Products  >  Details  >  Edit

Obviously, we would rather have it indicate which product we are editing. For the sake of this example, let’s just say that we want our breadcrumb trail to look like this.

Home  >  Products  >  Raincoat  >  Edit

That’s better – we now know which product we are editing. How can we do this? There are a couple of ways – use a SiteMapTitleAttribute or set the title manually in our controller action.

The SiteMapTitleAttribute can pull the data right out of your model or viewmodel class and set it for the current request to either the current node, or the parent of the current node. Here is what that would look like on our edit method.

[SiteMapTitle("Name", Target = AttributeTarget.ParentNode)]
public ActionResult Edit(int id)
{
    var db = new CRUDExample();
    var model = (from product in db.Product
                where product.Id == id
                select product).FirstOrDefault();
    return View(model);
}

This will scan our model for a public property named Name and use the value to set the parent node of the current node. Of course, db.Product must then have a property named Name for this to work. And then it just works. If we navigate to /Product/Edit/2, here is the resulting breadcrumb trail.

Home  >  Products  >  Raincoat  >  Edit

That covers the easy way, now let’s try doing it using code instead. This is very similar to what we did when setting the RouteValues, however there is one important difference – the match. We had to use a key to find our node before. That is because our goal was to force a match to the current request. Once we have forced the match, we can simply call SiteMaps.Current.CurrentNode to get a reference to the matching node. Here is what our code looks like in this case.

public ActionResult Edit(int id)
{
    var db = new CRUDExample();
    var model = (from product in db.Product
                where product.Id == id
                select product).FirstOrDefault();

    var node = SiteMaps.Current.CurrentNode;
    if (node != null && node.ParentNode != null)
    {
        node.ParentNode.Title = model.Name;
    }

    return View(model);
}

This is the equivalent to what the SiteMapTitleAttribute did for us above and the result is exactly the same. However, there are 2 important differences

  1. We can walk the node tree using ParentNode, ChildNodes, Descedants, and Ancestors to set properties on any node in the tree
  2. We have access to other properties that we can modify for the current request

The first one is pretty obvious – we could just call node.ParentNode.ParentNode to get a reference to the grandparent node, for example. Alternatively, we could use FindSiteMapNodeFromKey() like we did before to get access to any other node in the SiteMap.

Once we have a reference to the desired node, there are certain properties that are request-writable. If you try to write to any other property, you will get an exception because the cached sitemap is read-only. Here is a complete list of the request-writable properties (as of v4.2.0).

  • Title
  • Description
  • TargetFrame
  • ImageUrl
  • VisibilityProvider
  • Clickable
  • UrlResolver
  • CanonicalUrl
  • CanonicalKey
  • Route
  • Attributes
  • RouteValues

Note that Clickable and UrlResolver will only be useful if URL Resolution Caching is disabled for the node (see the previous section).

As an example, we could use the same method we used before to set a custom Attribute, like this.

public ActionResult Edit(int id)
{
    var node = SiteMaps.Current.CurrentNode;
    if (node != null)
    {
        node.Attributes["CustomKey"] = "SomeCustomValue";
    }

    var db = new CRUDExample();
    var model = (from product in db.Product
                 where product.Id == id
                 select product).FirstOrDefault();
    return View(model);
}

Attributes is a dictionary that can be used to store arbitrary information on the SiteMapNode. This can come in handy if you want to inject information into the node that will be used later in a custom display template or any other purpose. If we edit our /Views/Shared/DisplayTemplates/SiteMapNodeModel.cshtml file as follows, we will see our “SomeCustomValue” string in the resulting partial view.

@model MvcSiteMapProvider.Web.Html.Models.SiteMapNodeModel
@using System.Web.Mvc.Html
@using MvcSiteMapProvider.Web.Html.Models

@if (Model.Attributes.ContainsKey("CustomKey"))
{
    @Model.Attributes["CustomKey"].ToString()
}

@if (Model.IsCurrentNode && Model.SourceMetadata["HtmlHelper"].ToString() != "MvcSiteMapProvider.Web.Html.MenuHelper")  { 
    <text>@Model.Title</text>
} else if (Model.IsClickable) {
    if (string.IsNullOrEmpty(Model.Description))
    {
        <a href="@Model.Url" class="@Model.Attributes">@Model.Title</a>
    }
    else
    {
        <a href="@Model.Url" title="@Model.Description">@Model.Title</a>
    }
} else { 
    <text>@Model.Title</text>
}

And our output now looks like this – note the “SomeCustomValue” right before the word “Edit”.

image

Internally, the attributes dictionary is type <string, object>. This means that if you are using dynamic node providers or custom ISiteMapBuilder implementations, you can attach custom objects to the SiteMapNode. However, keep in mind that when added that way the nodes are stored in a global cache that is shared between all users of the web site, so you should not change the data in those objects when the SiteMapNode.IsReadOnly property is false.

On the other hand, when adding values using SiteMaps.Current to gain access to a node (from any view or controller action), the values are stored in the current request only, so it is safe to change them because they only apply to the current user anyway.

Fixing Node Visibility

The final finishing touch is fixing the visibility so our placeholder nodes don’t appear in our menu or sitemap. The most practical way to do this is to use the FilteredSiteMapNodeVisibilityProvider. There are two ways a visibility provider can be set, on the node directly or globally. Note that you can also register a global visibility provider and override it at the node level.

Registering the provider at the node level can be done using the visibilityProvider element in XML or the VisibilityProvider property when using MvcSiteMapNodeAttribute.

<mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="Products" controller="Product" action="Index">
        <mvcSiteMapNode title="Details" controller="Product" action="Details" visibilityProvider="MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider">
            <mvcSiteMapNode title="Edit" controller="Product" action="Edit" key="Product_Edit"/>
        </mvcSiteMapNode>
    </mvcSiteMapNode>
</mvcSiteMapNode>

Or

[MvcSiteMapNode(Title = "Edit", Key = "Edit", VisibilityProvider = "MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider")]
public ActionResult Edit(int id)
{
    ViewBag.id = id;

    // Implementation omitted...

    return View();
}

Note that the value is the fully qualified type name that is typically used by .NET to identify a type.

By default, no global default visibility provider is registered with MvcSiteMapProvider, but we can fix that by adding it to our configuration. For internal DI, we just add an appSettings value to our web.config.

<appSettings>
    <add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider"/>
</appSettings>

And for external DI, we need to inject the value into the constructor of SiteMapNodeVisibilityProviderStrategy. Using StrucutureMap, it would look like this.

// Visibility Providers
this.For<ISiteMapNodeVisibilityProviderStrategy>().Use<SiteMapNodeVisibilityProviderStrategy>()
    .Ctor<string>("defaultProviderName").Is("MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider");

Registering the visibility provider globally saves us a lot of bulk if we want to use the same visibility provider most of the time.

By default, the FilteredSiteMapNodeVisibilityProvider doesn’t do anything without extra configuration. I am not going to go into details about how it works, as there is already documentation on the wiki. But, in general we just need to use the type names (without the namespace) of the HTML helpers that we want to appear and the ones we want to exclude. We can also use a * as shorthand for “everything else”.

Here is what the configuration looks like for our simple example. We want our dummy nodes to appear on our sitemap path, but we don’t want them to appear anywhere else (including the /sitemap.xml endpoint).

<mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="Products" controller="Product" action="Index">
        <mvcSiteMapNode title="Details" controller="Product" action="Details" visibility="SiteMapPathHelper,!*">
            <mvcSiteMapNode title="Edit" controller="Product" action="Edit" visibility="SiteMapPathHelper,!*" key="Product_Edit"/>
        </mvcSiteMapNode>
    </mvcSiteMapNode>
</mvcSiteMapNode>

And being that FilteredSiteMapNodeVisibilityProvider is registered globally, we run our project and it now looks like this.

image

As you can see, our menu (at the top) and sitemap (at the bottom) HTML helpers are no longer displaying the Edit or Details nodes. This is fine because once we get to the “Products” page, our main navigation is based on an HTML table that is generated by the database data.

Be sure to check out the expanded functioning examples in the code download.

Summary

Today we covered how to make MvcSiteMapProvider track a user’s position. Or rather – how we make it act like it is tracking the user’s position. The best way to do this is to add every item we want to match as a node to the sitemap.

However, it isn’t the only way. We also took a look at how you can force every id to match a single node in order to fake the breadcrumb trail. It should be apparent that this is not the easiest way, but it can work for some very specific cases.

We talked about how to override the title and various other properties for the current request to fix other issues with setting up our “fake breadcrumb” scenario. The Attributes dictionary is an extension point where we can add our own custom data or even custom objects.

We rounded off our discussion with a brief look of how to set up node visibility.

Code download for this article: MvcSiteMapProvider-Remember-User-Position (2.46 MB)

Debugging an MvcSiteMapProvider Configuration

Setting up a configuration for MvcSiteMapProvider can be complex. Yet, I often see questions posted on StackOverflow or on GitHub that could have been easily answered had the poster taken the time to step through the code.

Today, I am going to talk about how to set up your project so you can step through the code of MvcSiteMapProvider to troubleshoot problems with your custom configuration. I have used the following procedure when helping others troubleshoot their configurations several times with great success.

Backing Up Your Solution

I shouldn’t have to tell you this, but you need to backup your solution before doing these steps. If you are using a great source code control solution (such as Git), then you should already be covered. If you are using a crappy one that doesn’t back up files unless they are part of your solution (such as Subversion), you should make a manual backup just in case. Either way, don’t say I didn’t warn you.

Later, when you are finished debugging, I recommend you revert back to the same state you started in and then fix your configuration using the information you gained from stepping through the code.

Getting the MvcSiteMapProvider Source Code

Next, you need to get the source code from GitHub and add it to a local directory (preferably somewhere outside of your solution). You can do this one of two ways.

  1. Clone the repository using Git.
  2. Download the zip file and unzip it to your local directory.

The second option is pretty self explanatory, and if you have never used Git it is probably the best solution.

If you are using Git, you can get the source code from running the command in your projects directory. Note this command will create a directory called MvcSiteMapProvider inside of the directory you are in.

git clone https://github.com/maartenba/MvcSiteMapProvider.git

Enabling NuGet Package Restore in Your Solution

MvcSiteMapProvider uses NuGet Package Restore at the solution level. As a result, the projects cannot be added to a solution that does not have NuGet Package Restore. If you already have NuGet Package Restore enabled, you will see a .nuget solution directory in Solution Explorer.

NuGet Package Restore

If you already see this in your solution, you can proceed to the next step.

To enable package restore in your solution, right-click on your solution in Solution Explorer, and click the “Enable NuGet Package Restore” option from the menu.

Enable NuGet Package Restore

You will get a confirmation dialog asking whether or not to proceed. Click Yes.

NuGet Package Restore Confirmation

After a few seconds, you will get a message box indicating that package restore is now enabled.

NuGet Package Restore Enabled

Alternatively, you can try disabling the NuGet package restore in your copy of the MvcSiteMapProvider solution. I haven’t tried this, but in theory it should also work by following these steps:

  1. Build the MvcSiteMapProvider solution so it downloads all of the NuGet packages.
  2. Disable NuGet Package Restore in the MvcSiteMapProvider solution.

The main thing is that NuGet package restore must be either enabled or disabled in both your solution and the MvcSiteMapProvider solution.

Adding MvcSiteMapProvider to your Solution

Now you need to add the MvcSiteMapProvider project to your solution. This can be done in a few simple steps. First, right-click on your solution and choose Add > Existing Project… from the menu.

Add Existing Project

Next, navigate to the directory you used previously when you got the source code. Continue drilling into the \src\MvcSiteMapProvider\MvcSiteMapProvider\ directory and locate the MvcSiteMapProvider.csproj file. Select it, and click the “Open” button.

Add MvcSiteMapProvider Project

MvcSiteMapProvider is now part of your solution.

Reconfigure your MvcSiteMapProvider References

The last step is to get your reference set up to use the MvcSiteMapProvider source code project rather than the DLL that was installed via NuGet. Note that you will need to do this for each project in your solution that has a reference to MvcSiteMapProvider that you want to debug.

Expand the references node of your project in Solution Explorer. Locate MvcSiteMapProvider, select it, and press the Delete key.

Remove MvcSiteMapProvider from your Project

Now right-click on the References node, and then click Add Reference… from the menu.

Add Reference to MvcSiteMapProvider

When the dialog opens, click the Solution > Projects tab on the left, and locate the MvcSiteMapProvider project. Check the box, then click OK.

Set Reference to MvcSiteMapProvider

Repeat the above procedure for each project that has a reference to MvcSiteMapProvider. When complete, you will be able to set breakpoints and step through the code of MvcSiteMapProvider.

Summary

There is usually no better source of information about why software isn’t working than stepping through the code. Today we had a look at how you can setup your own project to step through MvcSiteMapProvider in order to find out why your configuration isn’t working.

MvcSiteMapProvider 4.0 - Unit Testing with the SiteMaps Static Methods

It has been brought to my attention that using the static methods of the SiteMaps class in MvcSiteMapProvider is not unit testable – at least not at first glance.

The Problem

The problem is that when adding  a static method to your controller action method, there is no way to mock its behavior. That is true of most static methods but with MvcSiteMapProvider there is workaround.

Let’s build a small project to demonstrate this problem and how it can be fixed. Fire up Visual Studio and create a new project. Base it on the ASP.NET MVC 4 Web Application template and name the project “UnitTestingSiteMaps”.

image

Next, when the dialog appears for which MVC template to choose, select Internet Application, Razor, and check the box for “Create a unit test project” as shown. For this example, we will be using the built-in Visual Studio Unit Test framework.

image

After Visual Studio does it’s setup, we are left with 2 projects, an MVC project and a Unit Test project. But then, you probably already knew that.

Let’s install MvcSiteMapProvider. Right click on the UnitTestingSiteMaps project (not the actual unit test project, mind you) and click Manage NuGet Packages…

image

Type in “mvcsitemapprovider” in the search box and hit the Enter key. When the results show up, pick the “MvcSiteMapProvider MVC4” package from the list and click the Install button.

image

After installing MvcSitemapProvider, let’s open up the /Controllers/HomeController.cs file and add a new action method.

[HttpPost]
public ActionResult Index(FormCollection collection)
{
    // Invalidate the cache
    MvcSiteMapProvider.SiteMaps.ReleaseSiteMap();

    ViewBag.Message = "The cache was reset.";

    return View();
}

This is exactly the same method we used before in MvcSiteMapProvider 4.0 – Cache Configuration. Basically, all we are doing is clearing out the cache for our default sitemap instance and changing the return message to indicate the change was done.

Next, let’s add a form post with a submit button to our /Views/Home/Index.cshtml file in order to call this method.

@using (Html.BeginForm()) {
    <input type="submit" value="Unload Cache" />
}

If we run our project, we will see an Unload Cache button on the home page. Clicking the button will change the message that is shown in the subtitle of the page.

image

The Unit Test

Now, let’s make a unit test for our new action method. First, let’s open the /Controllers/HomeControllerTest.cs file in our test project and add a new test to verify our post action that we added to the home page.

[TestMethod]
public void Index_Post()
{
    // Arrange
    HomeController controller = new HomeController();
    var collection = new FormCollection();

    // Act
    ViewResult result = controller.Index(collection) as ViewResult;

    // Assert
    Assert.AreEqual("The cache was reset.", result.ViewBag.Message);
}

We are testing for the result of our message, the same as the default tests that are added by the Visual Studio template. Now we need to run our tests to see if the new one works. From the menu, click Test > Windows > Test Explorer.

image

The test explorer tab should open (most likely at the bottom). Inside of Test Explorer, click the “Run All” hyperlink.

image

The project will compile and then the tests will run. You can tell when they are finished because the line that appears below the search box will be either solid green or solid red all the way across horizontally. In our case, it is solid red because our new test failed.

In Test Explorer, click Index_Post under the Failed Tests heading so we can take a look at the error message.

image

Here is the result:

Test Name:    Index_Post
Test FullName:    UnitTestingSiteMaps.Tests.Controllers.HomeControllerTest.Index_Post
Test Source:    f:\Projects\ShiningTreasures_BlogPosts\MvcSiteMapProvider-Unit-Test-SiteMaps\UnitTestingSiteMaps\UnitTestingSiteMaps.Tests\Controllers\HomeControllerTest.cs : line 31
Test Outcome:    Failed
Test Duration:    0:00:00.0254307

Result Message:    
Test method UnitTestingSiteMaps.Tests.Controllers.HomeControllerTest.Index_Post threw exception: 
MvcSiteMapProvider.MvcSiteMapException: The SiteMapLoader has not been initialized. You must set the SiteMaps.Loader property during Application_Start in Global.asax if the 'MvcSiteMapProvider_UseExternalDIContainer' setting is set to 'true' in the AppSettings section of web.config.
Result StackTrace:    
at MvcSiteMapProvider.SiteMaps.ThrowIfLoaderNotInitialized()
   at MvcSiteMapProvider.SiteMaps.ReleaseSiteMap()
   at UnitTestingSiteMaps.Controllers.HomeController.Index(FormCollection collection) in f:\Projects\ShiningTreasures_BlogPosts\MvcSiteMapProvider-Unit-Test-SiteMaps\UnitTestingSiteMaps\UnitTestingSiteMaps\Controllers\HomeController.cs:line 22
   at UnitTestingSiteMaps.Tests.Controllers.HomeControllerTest.Index_Post() in f:\Projects\ShiningTreasures_BlogPosts\MvcSiteMapProvider-Unit-Test-SiteMaps\UnitTestingSiteMaps\UnitTestingSiteMaps.Tests\Controllers\HomeControllerTest.cs:line 37

What does that mean? It is telling us that our ISiteMapLoader instance is not initialized. Ok, what does that mean? For the answer, let’s have a look at the implementation of the SiteMaps class – that is, the class that we are calling the static method on in our Home controller action.

public class SiteMaps
{
    private static ISiteMapLoader loader;

    public static ISiteMapLoader Loader
    {
        set
        {
            if (value == null)
                throw new ArgumentNullException("value");
            if (loader != null)
                throw new MvcSiteMapException(Resources.Messages.SiteMapLoaderAlreadySet);
            loader = value;
        }
    }

    public static ISiteMap Current
    {
        get { return GetSiteMap(); }
    }

    public static ISiteMap GetSiteMap(string siteMapCacheKey)
    {
        ThrowIfLoaderNotInitialized();
        return loader.GetSiteMap(siteMapCacheKey);
    }

    public static ISiteMap GetSiteMap()
    {
        ThrowIfLoaderNotInitialized();
        return loader.GetSiteMap();
    }

    public static void ReleaseSiteMap(string siteMapCacheKey)
    {
        ThrowIfLoaderNotInitialized();
        loader.ReleaseSiteMap(siteMapCacheKey);
    }

    public static void ReleaseSiteMap()
    {
        ThrowIfLoaderNotInitialized();
        loader.ReleaseSiteMap();
    }

    private static void ThrowIfLoaderNotInitialized()
    {
        if (loader == null)
        {
            throw new MvcSiteMapException(Resources.Messages.SiteMapLoaderNotInitialized);
        }
    }
}

As we can see, the SiteMaps class is little more than a static helper to call our ISiteMapLoader instance. But more importantly than that, the ISiteMapLoader is injected into this class using property setter injection.

This explains why we are getting an error – ISiteMapLoader has not been set for our unit test. It also means that it can be mocked for unit testing.

The Solution

Now that we know that ISiteMapLoader can be mocked and where it can be injected, let’s fix our failing unit test. But we need to install a couple of dependencies for the fix.

First, we need to install a mocking framework. For this example, we will use Moq.  Go to the UnitTestingSiteMaps.Tests project (the actual unit test project) and right click References then click Manage NuGet Packages…

image

Enter “moq” in the search box and click Enter. Choose Moq from the list and click Install.

image

Next, we need to add a reference to MvcSiteMapProvider. But since we are installing MvcSiteMapProvider into a class library project (rather than an MVC project), we need to install the MvcSiteMapProvider.Core package so we don’t get all kinds of web related stuff (namely, the contents of the MvcSiteMapProvider.Web NuGet package) added to our DLL.

Once again, right click References and click Manage NuGet Packages…

image

Type in “mvcsitemapprovider” in the search box. Choose “MvcSiteMapProvider MVC4 Core” from the list, and then click the Install button.

image

When it is completed, open up the /Controllers/HomeControllerTest.cs file and add the following using statements to the top of the file:

using Moq;
using MvcSiteMapProvider;
using MvcSiteMapProvider.Loader;

Now find the Index_Post() method and add the following line just above the //Act comment:

SiteMaps.Loader = new Mock<ISiteMapLoader>().Object;

Our Unit Test should now look like this:

[TestMethod]
public void Index_Post()
{
    // Arrange
    HomeController controller = new HomeController();
    var collection = new FormCollection();
    SiteMaps.Loader = new Mock<ISiteMapLoader>().Object;

    // Act
    ViewResult result = controller.Index(collection) as ViewResult;

    // Assert
    Assert.AreEqual("The cache was reset.", result.ViewBag.Message);
}

Let’s now run our unit test again using the Run All link in Test Explorer, the same way we did before. Our failing unit test now passes with flying green colors.

image

Ok, you might argue that this technically isn’t a unit test because the static SiteMaps class isn’t being mocked (and you would be right). But in my experience with unit testing, making compromises is usually a requirement to get the job done.

You might also have a worry that the implementation of SiteMaps will change in a way that will break your tests. That is a valid concern, but my view of SiteMaps is that it doesn’t belong anyway. It is simply the logical cut point where we stopped our refactoring toward loose coupling, leaving the (mostly static) presentation layer relatively untouched.

The implementation might be added to over time, but it is unlikely to change (it is more likely to be removed at some point). Any real work will always be delegated to other classes that are not static.

Summary

Today, we took a look at the testability aspect of MvcSiteMapProvider. There are some static methods on the SiteMaps class that you might want to use in your controller actions, but doing so raises some nasty errors that break unit tests. There is a simple workaround – mock ISiteMapLoader and assign it to the SiteMaps.Loader property before running your unit test.

Code download for this article: MvcSiteMapProvider-Unit-Testing-SiteMaps (833 kb)

MvcSiteMapProvider 4.0 - Extending the Cache

If you analyze the new API of MvcSiteMapProvider 4.0, one of the first things you might notice is the “Extensibility” namespace is gone.  This was not done to intentionally confuse you, but it didn’t make a lot of sense to organize it this way when switching to a dependency injection supported design. The main difference is that a good sized chunk (roughly 70%) of the entire project is now extensible, and putting 70% of the classes - whether they are related or not - into one lump namespace is just wrong.

Of course, this extensibility is not free. You have to use an external dependency injection container (which implies you need to learn how to configure a DI container) in order to utilize it. Fortunately, many (if not most) of the users of MvcSiteMapProvider can simply install the NuGet package and use the built-in features and extend MvcSiteMapProvider only when necessary. In addition, our DI NuGet packages are kind of a way to “ride DI with the training wheels”. As we have seen in MvcSiteMapProvider 4.0 – Cache Configuration, upgrading a project to use external DI is not too difficult. As we will see today, this opens up a whole different world of extensibility.

Reviewing the Cache Configuration

Picking up where we left off in MvcSiteMapProvider 4.0 – Cache Configuration and it’s code download, let’s once again go to the /DI/StructureMap/Registries/MvcSiteMapProviderRegistry.cs file to see our cache configuration.

// Setup cache
SmartInstance<CacheDetails> cacheDetails;

this.For<ICacheProvider<ISiteMap>>().Use<AspNetCacheProvider<ISiteMap>>();

var cacheDependency =
    this.For<ICacheDependency>().Use<AspNetCompositeCacheDependency>()
        .EnumerableOf<ICacheDependency>().Contains(x =>
        {
            x.Type<AspNetFileCacheDependency>()
                .Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/Mvc.sitemap"));
            x.Type<AspNetFileCacheDependency>()
                .Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/SomeCacheDependency.txt"));
        });

cacheDetails =
    this.For<ICacheDetails>().Use<CacheDetails>()
        .Ctor<TimeSpan>("absoluteCacheExpiration").Is(TimeSpan.MinValue)
        .Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.FromSeconds(30))
        .Ctor<ICacheDependency>().Is(cacheDependency);

Ok, I have omitted some of the code examples from the code download that were commented out. Right now we have the AspNetCacheProvider and related ASP.NET cache dependencies configured. But there is one more extensibility point available with the RuntimeCacheProvider that I want to talk about, so let’s rewind and put the System.Runtime.Caching based provider back in its place.

// Setup cache
SmartInstance<CacheDetails> cacheDetails;

this.For<System.Runtime.Caching.ObjectCache>()
    .Use(s => System.Runtime.Caching.MemoryCache.Default);

this.For<ICacheProvider<ISiteMap>>().Use<RuntimeCacheProvider<ISiteMap>>();

var cacheDependency =
    this.For<ICacheDependency>().Use<RuntimeCompositeCacheDependency>()
        .EnumerableOf<ICacheDependency>().Contains(x =>
        {
            x.Type<RuntimeFileCacheDependency>()
                .Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/Mvc.sitemap"));
            x.Type<RuntimeFileCacheDependency>()
                .Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/SomeCacheDependency.txt"));
        });

cacheDetails =
    this.For<ICacheDetails>().Use<CacheDetails>()
        .Ctor<TimeSpan>("absoluteCacheExpiration").Is(TimeSpan.MinValue)
        .Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.FromSeconds(30))
        .Ctor<ICacheDependency>().Is(cacheDependency);

That’s more like it. Now, we did that specifically so we can talk about the first part – the System.Runtime.Caching.ObjectCache.

System.Runtime.Caching.ObjectCache

This is an abstract class provided by Microsoft that can be used to make a file-based cache, a distributed cache, a database cache, or pretty much any other type of cache you can dream up. This gives you the ability to host MvcSiteMapProvider in a wide range of environments such as high traffic sites, clouds and web farms.

As you can see, this abstraction plugs directly into MvcSiteMapProvider (it is actually injected by the DI container into RuntimeSiteMapProvider, but we will get to that in a minute), if you have a cache based on ObjectCache you can simply swap it apples-for-apples by changing the DI configuration.

this.For<System.Runtime.Caching.ObjectCache>()
    .Use(s => MyCustomCache);

This is the recommended extensibility point to use your own cache (if possible). We won’t be going into any implementation of this extensibility point, because in theory there should be plenty of cache implementations out there based on it (though they are a bit hard to find).

ICacheProvider<T>

This is the interface where a custom cache can be injected into MvcSiteMapProvider. You may be wondering why it is necessary to have yet another place to inject a cache, since ObjectCache should be enough. The answer is twofold:

  1. Microsoft didn’t provide a common interface between System.Runtime.Caching and System.Web.Caching and we needed to support both (primarily to support .NET 3.5).
  2. Since .NET doesn’t support multiple inheritance, it is not always possible to implement ObjectCache in a custom cache manager.

I mentioned it before, and I will mention it again – we recommend using ObjectCache if you can, primarily because other libraries are also likely to support it. ICacheProvider<T> wasn’t made to reinvent the wheel, but to work around the above two points.

Now that I have covered the bases, let’s have a look at the implementation of RuntimeCacheProvider<T> to see how you could implement a custom cache based upon it.

public class RuntimeCacheProvider<T>
    : ICacheProvider<T>
{
    public RuntimeCacheProvider(
        ObjectCache cache
        )
    {
        if (cache == null)
            throw new ArgumentNullException("cache");
        this.cache = cache;
    }
    private readonly ObjectCache cache;

    #region ICacheProvider<T> Members

    public event EventHandler<MicroCacheItemRemovedEventArgs<T>> ItemRemoved;

    public bool Contains(string key)
    {
        return cache.Contains(key);
    }

    public LazyLock Get(string key)
    {
        return (LazyLock)cache.Get(key);
    }

    public bool TryGetValue(string key, out LazyLock value)
    {
        value = this.Get(key);
        if (value != null)
        {
            return true;
        }
        return false;
    }

    public void Add(string key, LazyLock item, ICacheDetails cacheDetails)
    {
        var policy = new CacheItemPolicy();

        // Set timeout
        policy.Priority = CacheItemPriority.NotRemovable;
        if (IsTimespanSet(cacheDetails.AbsoluteCacheExpiration))
        {
            policy.AbsoluteExpiration = DateTimeOffset.Now.Add(cacheDetails.AbsoluteCacheExpiration);
        }
        else if (IsTimespanSet(cacheDetails.SlidingCacheExpiration))
        {
            policy.SlidingExpiration = cacheDetails.SlidingCacheExpiration;
        }

        // Add dependencies
        var dependencies = (IList<ChangeMonitor>)cacheDetails.CacheDependency.Dependency;
        if (dependencies != null)
        {
            foreach (var dependency in dependencies)
            {
                policy.ChangeMonitors.Add(dependency);
            }
        }

        // Setting priority to not removable ensures an 
        // app pool recycle doesn't unload the item, but a timeout will.
        policy.Priority = CacheItemPriority.NotRemovable;

        // Setup callback
        policy.RemovedCallback = CacheItemRemoved;

        cache.Add(key, item, policy);
    }

    public void Remove(string key)
    {
        cache.Remove(key);
    }

    #endregion

    private bool IsTimespanSet(TimeSpan timeSpan)
    {
        return (!timeSpan.Equals(TimeSpan.MinValue));
    }

    private void CacheItemRemoved(CacheEntryRemovedArguments arguments)
    {
        var item = arguments.CacheItem;
        var args = new MicroCacheItemRemovedEventArgs<T>(item.Key, ((LazyLock)item.Value).Get<T>(null));
        OnCacheItemRemoved(args);
    }

    protected virtual void OnCacheItemRemoved(MicroCacheItemRemovedEventArgs<T> e)
    {
        if (this.ItemRemoved != null)
        {
            ItemRemoved(this, e);
        }
    }
}

Let’s go over this method by method. If you notice, there is no thread safety handling in the provider – this is automatically handled for us by MicroCache<T>, so we don’t need to be concerned with it when building a custom provider.

RuntimeCacheProvider

First, we have the constructor, which takes an ObjectCache as an argument. In a nutshell, we are using constructor injection to add our one and only dependency in this class. The ObjectCache is set to a class level variable.

ItemRemoved Event

Next, we have our ItemRemoved event handler declaration. This event must be raised every time the cache expires or is cleared. If this event is not raised when the cache expires, MvcSiteMapProvider may have memory leaks because the SiteMap object and the nodes contain circular references that must be broken explicitly, so it is very important to ensure this event is raised both when the cache expires and when it is cleared.

Fortunately, ObjectCache has a callback that automatically gets fired in both of these cases, so it is not necessary to call this event explicitly in the Remove() method.

Contains Method

The implementation simply calls the Contains method of the ObjectCache.

In the case of AspNetCacheProvider or other types of dictionaries that don’t have their own Contains implementation, we can either fall back on Linq or check for a null reference.

return (Context.Cache[key] != null);

Get Method

The Get method also calls the Get method of the ObjectCache. There is one minor exception – we cast it to type LazyLock. The LazyLock is a class that acts like a trap to ensure that in high traffic sites where multiple requests could come in before the sitemap is built don’t try to build the sitemap multiple times. This design was inspired by the post: http://www.superstarcoders.com/blogs/posts/micro-caching-in-asp-net.aspx

TryGetValue Method

This follows the common .NET pattern for returning a boolean value indicating whether or not an operation succeeded. As you can see, we simply return false if the value is not in the cache. If the value is in the cache, we return both true and output the value.

Add Method

The add method is more interesting. This is where we utilize the CacheDetails object to set our timeout value, add our cache dependencies, and setup our cache item removed callback. We also set the priority so an IIS app pool recycle doesn’t cause our cache to expire prematurely.

Remove Method

Once again, simply delegate our call to ObjectCache.

The remaining code consists of helpers for the Add method and the callback to ensure the call is passed through (and types appropriately converted) from the ObjectCache to the ItemRemoved event.

Once we have constructed a custom implementation of ICacheProvider<T>, it can be injected by altering 1 line in our /DI/StructureMap/Registries/MvcSiteMapProviderRegistry.cs file.

this.For<ICacheProvider<ISiteMap>>().Use<MyCustomCacheProvider<ISiteMap>>();

The type we are passing in is the type that will be cached – in this case, ISiteMap. That is actually the only type the MvcSiteMapProvider caches internally.

ICacheDependency

Looking back at our StructureMap registry configuration, the next thing we do there is set our cache dependencies. We touched on this in MvcSiteMapProvider 4.0 – Cache Configuration, but we only showed how to alter the configuration to utilize the built-in cache dependencies, not to build our own. Let’s have a look under the hood of the RuntimeFileCacheDependency to see how we could implement one.

public class RuntimeFileCacheDependency
    : ICacheDependency
{
    public RuntimeFileCacheDependency(
        string fileName
        )
    {
        if (String.IsNullOrEmpty(fileName))
            throw new ArgumentNullException("fileName");

        this.fileName = fileName;
    }

    protected readonly string fileName;

    #region ICacheDependency Members

    public virtual object Dependency
    {
        get 
        {
            var list = new List<ChangeMonitor>();
            list.Add(new HostFileChangeMonitor(new string[] { fileName }));
            return list; 
        }
    }

    #endregion
}

This tiny class simply wraps an instance of the HostFileChangeMonitor. But what about the extra fluff to add it to a List<ChangeMonitor>? That is primarily to deal with multiplicity in the RuntimeCompositeCacheDependency class. Microsoft didn’t seem to have a loosely coupled way to deal with them, so this was the solution we came up with.

Note also the return type object. I couldn’t seem to find another way to generically pass the cache dependency classes around without polluting the other classes with dependencies I didn’t want. Generics didn’t seem to work in this case. If you can think of a better way, I am all ears. Please either leave your ideas in the comments or, better still, implement them and submit via pull request on GitHub.

Anyway, as you can see it would not be very complex to wrap another type of dependency. Once we get that task done, we simply alter our DI configuration, and we are all set.

var cacheDependency =
    this.For<ICacheDependency>().Use<MyCustomCacheDependency>();

Common Cache Extensibility Points

By now, you should be seeing a recurring theme on how to analyze one implementation and create a new one based on its interface, and then inject it into the configuration via DI. So, I will leave it as an exercise for the reader to explore some of the other cache functionality. However, I will document the other cache-related types here so you have an idea of what else can be done.

Type Description
IMicroCache<T> Provides thread-safe access to the cache.
ICacheDetails Provides caching instructions to the cache.
IRequestCache Provides access to the HTTP request-level cache.
ISiteMapCache Provides simpler syntax for passing a IMicroCache<ISiteMap>.
ISiteMapCacheKeyGenerator Generates a unique key for each ISiteMap cache. Each unique key will cause SiteMapLoader to create a new cache.
ISiteMapCacheKeyToBuilderSetMapper Provides the string name of a configured BuilderSet based on a siteMapCacheKey. Can be used to create mappings that are not 1-1.
ISiteMapLoader / SiteMapLoader Loads and unloads an ISiteMap to/from the cache.
ISiteMapCreator / SiteMapCreator Builds a specific ISiteMap based on a siteMapCacheKey.

A couple of important things to note:

  1. Unless otherwise setup in the default DI configuration, all classes wired up by default are named the same as their interface (minus the “I”).
  2. Nearly every method is marked virtual and nearly every type is declared protected, so you have the option of inheriting the class and overriding methods if it turns out to be an easier route.

Summary

Today we covered extending the cache of MvcSiteMapProvider. Extending MvcSiteMapProvider involves implementing an interface (or inheriting a type and overriding) and then injecting the new implementation via dependency injection.

The classes in MvcSiteMapProvider are short – most under 100 lines of code – and can be analyzed easily to determine how the interfaces can be exploited for your own deeds.

MvcSiteMapProvider 4.0 - Cache Configuration

One of the main things that was broken in MvcSiteMapProvider 3.x was the cache. There were bugs ranging from performance issues, to session data shared between users, to not being able to update the cache programmatically.

The cache of MvcSiteMapProvider was completely rewritten, not just to fix these issues, but also so it can be extended to support other use cases. It was rebuilt with multi-tenant applications and DI support in mind. Today, we will go into the basic configuration of the cache using the features that are included in the box, saving the cache extensibility for a future post.

In my previous post titled MvcSiteMapProvider 4.0 – SEO Features Tutorial, we built a demo project which we will be using as a starting point for this one.

Cache Settings using the Built-in DI Container

If we pick up where we left off in our last tutorial, the project we started with is using the internal DI container. There is just one setting that can be adjusted using internal DI – the timeout. This setting can be changed by using the MvcSiteMapProvider_CacheDuration web.config setting:

<appSettings>
    <add key="webpages:Version" value="2.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="PreserveLoginUrl" value="true" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    <add key="MvcSiteMapProvider_IncludeAssembliesForScan" value="Mvc4AndMvcSiteMapProvider" />
    <add key="MvcSiteMapProvider_UseExternalDIContainer" value="false" />
    <add key="MvcSiteMapProvider_ScanAssembliesForSiteMapNodes" value="true" />
    <add key="MvcSiteMapProvider_CacheDuration" value="5" />
</appSettings>

This setting gives us an absolute cache expiration in minutes. So in the example, we have our cache setup to expire every 5 minutes regardless of how many users access our site. 5 minutes is the default if this setting is omitted.

That’s all you can adjust using internal DI. To do anything more advanced requires an external dependency injection container.

Why Do I Need an External DI Container?

The original plan was to make the MvcSiteMapProvider configurable only with DI, but we started to realize that many developers are still not up to speed with this emerging technology and would still need an upgrade path from v3. So, the idea to make an internal DI container based on poor man’s DI was born (and the internal DI container was literally constructed in about 3 hours).

This is not intended to be the official way to configure MvcSiteMapProvider, but as a stepping stone for those who aren’t ready to make the investment of time into learning DI. That said, most of the configurability is not available using internal DI. We didn’t write our own DI container – that would be silly. Instead, we provided a simple substitution that can be used in the most basic scenarios. More advanced configuration requires the use of a DI container, but the good news is we don’t limit you to which DI containers you can use – your favorite one is probably supported.

We also have DI modules available (at the time of this writing) for Autofac, Ninject, SimpleInjector, StructureMap, Unity, and Windsor which can be used to integrate into an existing DI setup. We went the extra mile and created a composition root that can be automatically added to your project through the use of NuGet so you don’t have to be a DI genius to start with it. All of this code is “configuration” and should be viewed as such – you can edit it to tap into the real power of MvcSiteMapProvider and DI alike.

Recommended Reading: Dependency Injection in .NET

Upgrading the Project from Internal DI to External DI

Upgrading is pretty straightforward, but as mentioned there are 2 upgrade paths.

Upgrade Path 1 - If your project already has DI

The upgrade path to use if your project already uses DI is to install one our modules (or in case we don’t have one for your DI container, to build your own – and don’t forget to contribute it via pull request). The modules are available via NuGet. As an example, to install the StructureMap modules, run this command in Package Manager Console:

Install-Package MvcSiteMapProvider.MVC4.DI.StructureMap.Modules

There are a few additional steps you need to do in order to wire it to your existing setup and start the gears turning, but I won’t be getting into that here. That is a subject for a whole other post. In the meantime, have a look at the closed issues at GitHub that refer to setup using DI.

Upgrade Path 2 - If your project doesn’t use DI

In this case it would be preferable to install one of the full DI packages including the composition root. You can flip a coin and decide which DI container to use (or do some research, but in general they all do pretty much the same thing).

This is what we will be using today in this demo. So in our demo project, run this command in Package Manager Console to upgrade from internal DI to StructureMap:

Install-Package MvcSiteMapProvider.MVC4.DI.StructureMap

I am not going to go into depth into what that command did, but the important thing to know now is that now you are running on StructureMap and your DI configuration can be changed in /DI/StructureMap/Registries/MvcSiteMapProviderRegistry.cs. The exact path and name of the module varies slightly from one DI container configuration to the next.

I am not going to go over every DI container as they are all similar and can generally be translated from one container’s syntax to another. So keep this in mind if you are using another container.

Let’s run the project to make sure everything is still working as it was before.

image

You can see that it works, but what you don’t see is that it is now rewired to use external DI.

IMPORTANT NOTE: The external DI container doesn’t use the web.config settings for configuration out of the box. In addition, the NuGet package doesn’t automatically migrate the settings you have configured in web.config to the external DI container, nor does it remove the old settings. Instead, that step must be done manually.

Since we haven’t edited any MvcSiteMapProvider settings yet in this demo, it is safe for us to go ahead and delete them, leaving only MvcSiteMapProvider_UseExternalDIContainer, because it is required to make use of the external DI container.

<appSettings>
    <add key="webpages:Version" value="2.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="PreserveLoginUrl" value="true" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    <add key="MvcSiteMapProvider_UseExternalDIContainer" value="true" />
</appSettings>

Normally, you would want to migrate these values into /DI/StructureMap/Registries/MvcSiteMapProviderRegistry.cs, where the configuration is done. Alternatively, you could edit that file to make it read the settings from web.config or build your own custom web.config settings.

Cache Settings using StructureMap External DI Container

Now it’s time to play with the cache settings to see what we can achieve out of the box. First of all, let’s take a look at the stock caching configuration in the /DI/StructureMap/Registries/MvcSiteMapProviderRegistry.cs file.

// Setup cache
SmartInstance<CacheDetails> cacheDetails;

this.For<System.Runtime.Caching.ObjectCache>()
    .Use(s => System.Runtime.Caching.MemoryCache.Default);

this.For<ICacheProvider<ISiteMap>>().Use<RuntimeCacheProvider<ISiteMap>>();

var cacheDependency =
    this.For<ICacheDependency>().Use<RuntimeFileCacheDependency>()
        .Ctor<string>("fileName").Is(absoluteFileName);

cacheDetails =
    this.For<ICacheDetails>().Use<CacheDetails>()
        .Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
        .Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
        .Ctor<ICacheDependency>().Is(cacheDependency);

Basically, we have a System.Runtime.Caching.ObjectCache that maps to our default MemoryCache. We also have a RuntimeCacheProvider<T> that implements ICacheProvider<T>. Both of these are extensibility points that can be used to replace the stock implementation. But that is beyond the scope of this article – for now we are focusing on cache configuration, not cache extensibility.

We also have an ICacheDependency instance that is set to use RuntimeFileCacheDependency which is passed the absolute file name of the Mvc.sitemap file. This class wraps the System.Runtime.Caching.ChangeMonitor framework that is supplied by .NET.

Last but not least, is the ICacheDetails interface setup to use CacheDetails. This is where we will start.

CacheDetails

First of all, note the settings that are passed into the constructor of CacheDetails. We have an absolute cache expiration, a sliding cache expiration, and a cache dependency. These map directly to their counterparts in the .NET framework and if you have used one of the built-in caching mechanisms then they probably look familiar.

The internal DI container is only capable of configuring the absolute cache expiration. This isn’t always ideal as sometimes we want the cache to stick around longer as long as users keep accessing it. That is what the absolute cache expiration is for.

Both absolute cache expiration and sliding cache expiration are TimeSpan data type values, and only one or the other should be specified, leaving the other one as TimeSpan.MinValue. If both are configured, absolute cache expiration will take precedence.

Let’s setup our cache to have a sliding cache expiration of 30 seconds just so we can see how that is done:

cacheDetails =
    this.For<ICacheDetails>().Use<CacheDetails>()
        .Ctor<TimeSpan>("absoluteCacheExpiration").Is(TimeSpan.MinValue)
        .Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.FromSeconds(30))
        .Ctor<ICacheDependency>().Is(cacheDependency);

Unfortunately, this is not something that is easy to demonstrate, but it has been thoroughly tested and works well.

Note: You can effectively shut off caching by setting the absoluteCachExpiration to TimeSpan.Zero.

ICacheDependency

This is where we set up our dependencies. A dependency is a resource that will cause the sitemap cache to expire immediately when the resource is changed. By default, we just have a single Mvc.sitemap file so that is the only dependency. But MvcSiteMapProvider can be setup to use sources other than files (such as databases), which could be used as well (NOTE: at the time of this writing, there is no SqlCacheDependency support available out of the box, but it can be easily added if needed by implementing ICacheDependency yourself).

Using the runtime cache provider, there are only 3 options included in the box: RuntimeFileCacheDependency, RuntimeCompositeCacheDependency, and NullCacheDependency. Note that there is also another caching option in the box – AspNetCacheProvider – which I will be covering shortly.

We have already seen the use of RuntimeFileCacheDependency. It takes an absolute file name (not the virtual file name) of a file as a constructor argument. Simply put, if the file changes on disk, the cache is unloaded and rebuilt during the next user request.

The RuntimeCompositeCacheDependency is how we add multiplicity to the ICacheDependency interface. For example, if we want our cache to be dependent on the files Mvc.sitemap and SomeCacheDependency.txt, we would configure our cache dependency like this:

var cacheDependency =
    this.For<ICacheDependency>().Use<RuntimeCompositeCacheDependency>()
        .EnumerableOf<ICacheDependency>().Contains(x =>
        {
            x.Type<RuntimeFileCacheDependency>()
                .Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/Mvc.sitemap"));
            x.Type<RuntimeFileCacheDependency>()
                .Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/SomeCacheDependency.txt"));
        });

The final option is a NullCacheDependency. This class uses the Null Object Pattern to tell MvcSiteMapProvider that we don’t want to use any cache dependencies, which is useful if we are only setting up our SiteMap in code without any XML. Here is how that can be configured:

var cacheDependency =
    this.For<ICacheDependency>().Use<NullCacheDependency>();

AspNetCacheProvider

This option is an alternative to using the RuntimeCacheProvider when configuring MvcSiteMapProvider. AspNetCacheProvider is based on System.Web.Caching, which until .NET 4.0 was the only caching option in the .NET framework. This is why it is configured by default when installing MvcSiteMapProvider in a .NET 3.5 project.

While this option is required for supporting .NET 3.5, it is also perfectly valid to use it in a .NET 4.0 or later project. Let’s change our project to use ASP.NET caching as a demo.

// Before
this.For<System.Runtime.Caching.ObjectCache>()
    .Use(s => System.Runtime.Caching.MemoryCache.Default);

this.For<ICacheProvider<ISiteMap>>().Use<RuntimeCacheProvider<ISiteMap>>();

var cacheDependency =
    this.For<ICacheDependency>().Use<RuntimeCompositeCacheDependency>()
        .EnumerableOf<ICacheDependency>().Contains(x =>
        {
            x.Type<RuntimeFileCacheDependency>()
                .Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/Mvc.sitemap"));
            x.Type<RuntimeFileCacheDependency>()
                .Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/SomeCacheDependency.txt"));
        });

// After
this.For<ICacheProvider<ISiteMap>>().Use<AspNetCacheProvider<ISiteMap>>();

var cacheDependency =
    this.For<ICacheDependency>().Use<AspNetCompositeCacheDependency>()
        .EnumerableOf<ICacheDependency>().Contains(x =>
        {
            x.Type<AspNetFileCacheDependency>()
                .Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/Mvc.sitemap"));
            x.Type<AspNetFileCacheDependency>()
                .Ctor<string>("fileName").Is(HostingEnvironment.MapPath("~/SomeCacheDependency.txt"));
        });

Let’s review the changes. The assignment of System.Runtime.Caching.ObjectCache has been removed because it is no longer required. The ICacheProvider<T> was changed from a RuntimeCacheProvider<T> to an AspNetCacheProvider<T>. Also, the cache dependencies were changed to their ASP.NET counterparts.

It is important to note that the Runtime components and ASP.NET components can be substituted, but cannot be mixed. If you use AspNetCacheProvider, you must also use the ASP.NET cache dependencies. The only exception is the NullCacheDependency, which works in both cases.

With this configuration change we did a 1 for 1 exchange. No functionality changed, only the place where the sitemap is cached.

Expiring the Cache Programmatically

One thing that was missing in v3 was the ability to reset the cache using code. This has been added in v4. You can now manually invalidate the cache using the ReleaseSiteMap() method.

MvcSiteMapProvider.SiteMaps.ReleaseSiteMap();

This, just like the cache dependency will cause the sitemap to unload from memory immediately and it will be rebuilt and cached again on the next user’s request.

There is also an overload that accepts a siteMapCacheKey as an argument, which is how you can specify which cache to expire if there are multiple. See the Multiple SiteMaps in One Application document in the wiki for more information about the siteMapCacheKey.

Typically, you would want to tie this to the “save” button that causes the data to update in a database. For our demo, we will just add a button to the home page to invalidate the cache.

First, open up the /Controllers/HomeController.cs file and add the following code:

[HttpPost]
public ActionResult Index(FormCollection collection)
{
    // Invalidate the cache
    MvcSiteMapProvider.SiteMaps.ReleaseSiteMap();

    ViewBag.Message = "The cache was reset.";

    return View();
}

Next, add a form to the bottom of the home page with a submit button to call the action method we just created.

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()

    <input type="submit" value="Unload Cache" />
}

Now if we run our project, we will see an “Unload Cache” button on the bottom of the home page. Clicking the button will update the status message to indicate the cache was reloaded. Actually, the cache is unloaded by the code we added, but when returning the View it is rebuilt again as the _Layout.cshtml is executed (because it contains our sitemap HTML helpers).

Summary

The cache in MvcSiteMapProvider was completely rewritten from the ground up. This fixed a lot of really annoying bugs as well as made the cache both more configurable and extensible. Today we covered the configuration part, we will be getting into cache extensibility later.

An external DI container isn’t required to use MvcSiteMapProvider, but it is required to access most of the extensibility and configuration, including that of the cache. We have provided a simple DI container as a stepping stone for those who aren’t up to speed on DI, but it is easy to outgrow it if your requirements become more complex, in which case you must use a 3rd party DI container.

Code download for this article: MvcSiteMapProvider-Cache-Configuration (840 kb)