0
votes

It looks like the IsCurrentNode property of the MVCSitemapProvider is case sensitive to the URL.

E.g.

https://localhost/MvcApplication1/customer/exportdata (no match)
https://localhost/MvcApplication1/Customer/ExportData (works)

Since the case of an URL is a bit difficult to ensure, is there a way around this?

This is how my sitemap currently looks like:

<mvcSiteMapNode title="Home" controller="Home" action="Index" clickable="false">
    <mvcSiteMapNode title="Meine Aufträge" controller="Home" action="Index">
        <mvcSiteMapNode title="Übersicht" controller="Customer/Orders" action="Index"/>
        <mvcSiteMapNode title="Rechnungen" controller="Customer/Bills" action="Index" roles="CompanyAdmin"/>
        <mvcSiteMapNode title="Export" controller="Customer/ExportData" action="Index"/>
    </mvcSiteMapNode>
    <mvcSiteMapNode title="Firmenadministration" controller="Home" action="Index">
        <mvcSiteMapNode title="Übersicht" controller="AllOrders" action="About"/>
    </mvcSiteMapNode>
    <mvcSiteMapNode title="Freelancer" url="http://www.myblog1.com" >
        <mvcSiteMapNode title="Übersicht" url="http://www.myblog2.com" />
        <mvcSiteMapNode title="Rechnungen" url="http://www.myblog3.com" />
    </mvcSiteMapNode>
</mvcSiteMapNode>

Parts of it is just test data.

1
controller="Customer/Orders" is not supported. If you need to add an area, there is a separate field area="Customer" controller="Orders". That is probably why your case-insensitive matching is not working.NightOwl888
That was it! Sorry, didn't see that.Remy
Looks like I need to add some checking for invalid characters (such as "/") in the controller, action, and area fields to ensure this kind of confusion can't happen.NightOwl888

1 Answers

1
votes

Enforcing lowercase URLs is pretty easy with .NET 4.5, as long as you use the URL Helper to build your URLs:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.LowercaseUrls = true;
    ...
}

RouteCollection.LowercaseUrls

Also, it is generally a best practice to use lower case URLs for better SEO and caching support.

I suspect this has not been reported before now because most people use the @Html.ActionLink, @Html.RouteLink, or @Html.Url helpers to build their URLs, which always makes them the same case, so the comparison will always work.

I'd appreciate it if you report this as a new issue @ GitHub so we can add this to the work queue, as I agree this is a bug that needs to be addressed.

The underlying problem is twofold:

  1. The SiteMap stores the URLs as a key in one of the dictionaries, which does a case sensitive match.
  2. The URL path is compared (case sensitive) first before the case insensitive routes are compared.

This means that if all of the URLs were generated lowercase, the problem would go away. This is because MvcSiteMapProvider uses the MVC UrlHelper class to generate the URLs.

However, another potential fix would be to rearrange the order in which the comparisons are done so the routes are compared first. You can accomplish this by inheriting RequestCacheableSiteMap and overriding FindSiteMapNode().

// Original
protected virtual ISiteMapNode FindSiteMapNode(HttpContextBase httpContext)
{
    // Match RawUrl
    var node = this.FindSiteMapNodeFromRawUrl(httpContext);

    // Try MVC
    if (node == null)
    {
        node = this.FindSiteMapNodeFromMvc(httpContext);
    }

    // Try ASP.NET Classic (for interop)
    if (node == null)
    {
        node = this.FindSiteMapNodeFromAspNetClassic(httpContext);
    }

    // Try the path without the querystring
    if (node == null)
    {
        node = this.FindSiteMapNode(httpContext.Request.Path);
    }

    // Check accessibility
    return this.ReturnNodeIfAccessible(node);
}

// Fixed
protected override ISiteMapNode FindSiteMapNode(HttpContextBase httpContext)
{
    // Match MVC
    var node = this.FindSiteMapNodeFromMvc(httpContext);

    // Try RawUrl
    if (node == null)
    {
        node = this.FindSiteMapNodeFromRawUrl(httpContext);
    }

    // Try ASP.NET Classic (for interop)
    if (node == null)
    {
        node = this.FindSiteMapNodeFromAspNetClassic(httpContext);
    }

    // Try the path without the querystring
    if (node == null)
    {
        node = this.FindSiteMapNode(httpContext.Request.Path);
    }

    // Check accessibility
    return this.ReturnNodeIfAccessible(node);
}

You can then either inherit SiteMapFactory or create your own ISiteMapFactory that returns your SiteMap type and inject it with external DI. This solution won't work if using the internal DI container.

using System;
using MvcSiteMapProvider.Builder;
using MvcSiteMapProvider.Web.Mvc;
using MvcSiteMapProvider.Web;

public class MySiteMapFactory
    : ISiteMapFactory
{
    public MySiteMapFactory(
        ISiteMapPluginProviderFactory pluginProviderFactory,
        IMvcResolverFactory mvcResolverFactory,
        IMvcContextFactory mvcContextFactory,
        ISiteMapChildStateFactory siteMapChildStateFactory,
        IUrlPath urlPath,
        IControllerTypeResolverFactory controllerTypeResolverFactory,
        IActionMethodParameterResolverFactory actionMethodParameterResolverFactory
        )
    {
        if (pluginProviderFactory == null)
            throw new ArgumentNullException("pluginProviderFactory");
        if (mvcResolverFactory == null)
            throw new ArgumentNullException("mvcResolverFactory");
        if (mvcContextFactory == null)
            throw new ArgumentNullException("mvcContextFactory");
        if (siteMapChildStateFactory == null)
            throw new ArgumentNullException("siteMapChildStateFactory");
        if (urlPath == null)
            throw new ArgumentNullException("urlPath");
        if (controllerTypeResolverFactory == null)
            throw new ArgumentNullException("controllerTypeResolverFactory");
        if (actionMethodParameterResolverFactory == null)
            throw new ArgumentNullException("actionMethodParameterResolverFactory");

        this.pluginProviderFactory = pluginProviderFactory;
        this.mvcResolverFactory = mvcResolverFactory;
        this.mvcContextFactory = mvcContextFactory;
        this.siteMapChildStateFactory = siteMapChildStateFactory;
        this.urlPath = urlPath;
        this.controllerTypeResolverFactory = controllerTypeResolverFactory;
        this.actionMethodParameterResolverFactory = actionMethodParameterResolverFactory;
    }

    protected readonly ISiteMapPluginProviderFactory pluginProviderFactory;
    protected readonly IMvcResolverFactory mvcResolverFactory;
    protected readonly IMvcContextFactory mvcContextFactory;
    protected readonly ISiteMapChildStateFactory siteMapChildStateFactory;
    protected readonly IUrlPath urlPath;
    protected readonly IControllerTypeResolverFactory controllerTypeResolverFactory;
    protected readonly IActionMethodParameterResolverFactory actionMethodParameterResolverFactory;


    #region ISiteMapFactory Members

    public virtual ISiteMap Create(ISiteMapBuilder siteMapBuilder, ISiteMapSettings siteMapSettings)
    {
        var routes = mvcContextFactory.GetRoutes();
        var requestCache = mvcContextFactory.GetRequestCache();

        // IMPORTANT: We need to ensure there is one instance of controllerTypeResolver and 
        // one instance of ActionMethodParameterResolver per SiteMap instance because each of
        // these classes does internal caching.
        var controllerTypeResolver = controllerTypeResolverFactory.Create(routes);
        var actionMethodParameterResolver = actionMethodParameterResolverFactory.Create();
        var mvcResolver = mvcResolverFactory.Create(controllerTypeResolver, actionMethodParameterResolver);
        var pluginProvider = pluginProviderFactory.Create(siteMapBuilder, mvcResolver);

        return new MySiteMap(
            pluginProvider,
            mvcContextFactory,
            siteMapChildStateFactory,
            urlPath,
            siteMapSettings,
            requestCache);
    }

    #endregion
}

And then inject your factory something like this in the MvcSiteMapProvider DI module (StructureMap example shown):

For<ISiteMapFactory>().Use<MySiteMapFactory>();

Do note you will have to re-implement the constructor of RequestCacheableSiteMap so all of its dependencies are passed into the object just as they are in the MySiteMapFactory implementation.

I haven't tested this solution to determine if there are any side effects, especially if you are using a mixed bag of routes and URLs.