MVCSitemapProvider IsCurrentNode чувствителен к регистру?

Похоже, что свойство IsCurrentNode MVCsitemapProvider чувствительно к регистру URL-адреса.

E.g.

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

Поскольку случай URL-адреса немного сложно обеспечить, есть ли способ обойти это?

Вот как сейчас выглядит моя карта сайта:

<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>

Частично это просто тестовые данные.


person Remy    schedule 22.11.2013    source источник
comment
controller=Customer/Orders не поддерживается. Если вам нужно добавить область, есть отдельное поле область = Контроллер клиентов = Заказы. Вероятно, поэтому ваше сопоставление без учета регистра не работает.   -  person NightOwl888    schedule 29.11.2013
comment
Вот оно! Извините, не видел этого.   -  person Remy    schedule 30.11.2013
comment
Похоже, мне нужно добавить некоторую проверку на недопустимые символы (такие как /) в полях контроллера, действия и области, чтобы исключить такую ​​путаницу.   -  person NightOwl888    schedule 30.11.2013


Ответы (1)


В .NET 4.5 довольно легко ввести URL-адреса в нижнем регистре, если вы используете помощник URL для создания своих URL-адресов:

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

RouteCollection.LowercaseUrls

Кроме того, как правило, рекомендуется использовать URL-адреса в нижнем регистре для улучшения SEO и поддержки кэширования.

Я подозреваю, что об этом раньше не сообщалось, потому что большинство людей используют помощники @Html.ActionLink, @Html.RouteLink или @Html.Url для создания своих URL-адресов, что всегда делает их одинаковыми, поэтому сравнение всегда будет работать.

Буду признателен, если вы сообщите об этом как о новой проблеме @ GitHub, чтобы мы можете добавить это в рабочую очередь, так как я согласен, что это ошибка, которую необходимо устранить.

Основная проблема двояка:

  1. SiteMap хранит URL-адреса в качестве ключа в одном из словарей, что обеспечивает соответствие с учетом регистра.
  2. Путь URL-адреса сравнивается (с учетом регистра) перед сравнением маршрутов без учета регистра.

Это означает, что если бы все URL-адреса были сгенерированы в нижнем регистре, проблема исчезла бы. Это связано с тем, что MvcSiteMapProvider использует класс MVC UrlHelper для создания URL-адресов.

Однако другим потенциальным решением может быть изменение порядка, в котором выполняются сравнения, чтобы маршруты сравнивались в первую очередь. Этого можно добиться, унаследовав RequestCacheableSiteMap и переопределив 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);
}

Затем вы можете либо наследовать SiteMapFactory, либо создать свой собственный ISiteMapFactory, который возвращает ваш тип SiteMap и внедряет его с помощью внешнего внедрения зависимостей. Это решение не будет работать при использовании внутреннего контейнера внедрения зависимостей.

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
}

Затем введите что-то вроде этого в модуль DI MvcSiteMapProvider (показан пример StructureMap):

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

Обратите внимание, что вам придется повторно реализовать конструктор RequestCacheableSiteMap, чтобы все его зависимости передавались в объект точно так же, как в реализации MySiteMapFactory.

Я не тестировал это решение, чтобы определить, есть ли какие-либо побочные эффекты, особенно если вы используете смешанный набор маршрутов и URL-адресов.

person NightOwl888    schedule 23.11.2013
comment
Нормально будет сделать. Но с URL-адресами в нижнем регистре SiteMap все равно не будет соответствовать им, верно? В любом случае, я могу исправить это в то же время? Мы используем ActionLink и прочее, но люди также приходят напрямую по URL-адресам, и тогда они могут быть не всегда правы. - person Remy; 24.11.2013
comment
Кстати, вы можете использовать перезапись URL-адресов, чтобы пользователи перенаправлялись на URL-адрес в нижнем регистре - ruslany.net/2009/04/10-url-rewriting-tips-and-tricks - person NightOwl888; 24.11.2013
comment
Спасибо за информацию. Я пытался использовать подход route.LowercaseUrls = true. Но это просто сломало его во всех ситуациях. Ваше второе предложение выглядело немного сложным, поэтому я решил получить исходный код и посмотреть, смогу ли я его исправить. Но просматривая код, я закончил на MatchesActionMethodParameter и MatchesValue в RouteValueDictionary (исходя из FindSiteMapNodeFromMvc). Оба метода выполняют сравнение InvariantCultureIgnoreCase. Так что я немного смущен тем, почему это еще не работает? Глядя на MatchesValue: this[key] = Customer/ExportData, но значением является только exportdata. - person Remy; 25.11.2013