Nested wildcard item resolution issue

(Observed on Sitecore 9.3 & 10.2)

The Problem

We use nested wildcard items of the form:

/* (year)
   /* (month)
      /* (article)

We use these to handle news syndication between our websites. So we handle requests like:

  • /2023/March/some-news-article
  • /2022/December/an-interesting-article-about-something-else

These requests are caught by the * (article) wildcard item where the actual source article is retrieved from another place in the content tree. That works.

However, a site can also combine local news with syndicated news like this:

(Sample Site Content Tree)

/* (year)
   /*(month)
      /* (article)
/2023
   /May
      /my-local-article

The combination of local & syndicated news is where the issue crops up. A request that should return the syndicated news article * (article) page item instead returns the * (year) page item. This only happens if there is a local news YEAR that matches the requested syndicated news YEAR.

For example, given the sample site above which has an article in the year 2023, if I make a request for /2023/March/some-syndicated-article Sitecore will instead return the * (year) page item for that request.

Steps to Reproduce:

  1. Create the * (year)/* (month)/* (article) wildcard items and make sure they have explicitly different Titles so you can tell them apart when browsing each page.
  2. Verify that you can make any request and get the desired wildcard page item. For example:
/2023                  --> should return * (year) page
/2023/june             --> should return * (month) page
/2023/june/my-article  --> should return * (article) page
  1. Now create a “2023” page item as a sibling to * (year)
  2. Retry your tests from step #2. Note that they all return * (year) now

Solution #1 (from Sitecore Support)

Sitecore support logged this issue as a bug (reference # 587340) and provided an alternative solution to the one I came up with below that is much simpler if you can use it.

As we checked further, we found a simpler workaround to resolve the issue. It seems like this behavior is affected by the setting of ItemResolving.FindBestMatch, defined inside Sitecore.config file. By default, this setting is set to DeepScan, with the its description is as below:

DeepScan - item resolving will try to match an item by scanning the whole content tree.

This reported behavior does not happen when you set this config to Enabled. You can use the the Sitecore config patch file to customize this configuration as a workaround.

I tried this and it worked for me. I did follow up and ask about the difference between “DeepScan” vs. “Enabled” and when I would need to use one over the other. Here was their response:

As described inside the Sitecore.config, this ItemResolving.FindBestMatch setting is used to specify the mode Sitecore will use to resolve and find items with names that contain the values defined in the ‘find‘ and ‘replaceWith‘ properties in the <encodeNameReplacements> section, for example, spaces and dashes.

Generally, this setting should not effect the process of resolving wildcard item. However, since the asterisk symbol (*) is one of the character that involve character encoding, the logic of resolving item might be affected, which might caused this unexpected behavior.

For Enabled setting, the item resolving logic will try to find a matching item with name that includes values defined, as described above, on each nesting level separately.
While for DeepScan setting, the item resolving will try to match an item with name that includes values defined, as described above, by scanning the whole content tree.

The value of this setting does effect the performance of resolving item that contains characters defined in <encodeNameReplacement> section.
Unless you have a lot of item on the same level with names whose only difference is that they contain the defined characters, switching from DeepScan to Enabled (and vice versa) should not have any noticeable performance impact between them.

Irfan Mokhtar – Sitecore Support

Solution #2 My Temporary Solution

In the course of troubleshooting this I have devised a workaround which I’ll share here in case it helps someone else who encounters this somewhat obscure issue.

Ultimately the issue resides in Sitecore.Data.ItemResolvers.MixedItemNameResolver in the ResolveRecursive method… at least this is where I chose to fix it.

As the method name suggests, ResolveRecursive attempts to resolve a list of subpaths to a leaf item. The issue seems to be that when it encounters a wildcard item “*” it just assumes that is a sufficiently deep match and doesn’t check for any nested wildcard items “*”. My code swaps out the red circled code for the following which recursively attempts to resolve nested wildcard items:

            if (subPaths.Count == 1)
            {
                item = child;
            }
            else
            {
                var range = subPaths.GetRange(1, subPaths.Count - 1);
                item = ResolveRecursive(child, range);
            }

A relatively isolated change, but in order to get it in place I ended up with the following…

ItemResolver.cs

using Sitecore.Abstractions;
using Sitecore.Configuration;
using Sitecore.Data.ItemResolvers;
using Sitecore.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Drexel.Foundation.Framework.TempFix.DR1539.Pipelines.HttpRequest
{
    public class ItemResolver : Sitecore.Pipelines.HttpRequest.ItemResolver
    {
        public ItemResolver() :
            this((BaseItemManager)ServiceLocator.ServiceProvider.GetService(typeof(BaseItemManager)),
                 (ItemPathResolver)ServiceLocator.ServiceProvider.GetService(typeof(ItemPathResolver)))
        { }

        public ItemResolver(BaseItemManager itemManager, ItemPathResolver pathResolver) :
            this(itemManager, pathResolver, Settings.ItemResolving.FindBestMatch)
        { }


        protected ItemResolver(BaseItemManager itemManager, ItemPathResolver pathResolver, MixedItemNameResolvingMode itemNameResolvingMode) : base(itemManager, pathResolver, itemNameResolvingMode)
        {

            var enabledOrDeepScan = (itemNameResolvingMode & MixedItemNameResolvingMode.Enabled) == MixedItemNameResolvingMode.Enabled;

            if (enabledOrDeepScan)
                base.PathResolver = new DR1539.Data.ItemResolvers.MixedItemNameResolver(pathResolver);
        }
    }
}

MixedItemNameResolver.cs

using Sitecore;
using Sitecore.Configuration;
using Sitecore.Data.ItemResolvers;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Reflection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;

namespace Drexel.Foundation.Framework.TempFix.DR1539.Data.ItemResolvers
{
    public class MixedItemNameResolver : Sitecore.Data.ItemResolvers.MixedItemNameResolver
    {
        private static MethodInfo _candidateMatchesMethod = null;
        private MixedItemNameResolvingMode _resolvingMode = Settings.ItemResolving.FindBestMatch;

        public MixedItemNameResolver(ItemPathResolver defaultResolver) : base(defaultResolver)
        {
            _candidateMatchesMethod = this.GetType().BaseType.GetMethod("CandidateMatches", BindingFlags.NonPublic | BindingFlags.Instance);
        }


        protected override Item ResolveRecursive(Item root, List<string> subPaths)
        {
            Assert.ArgumentNotNull(root, "root");
            Assert.ArgumentNotNull(subPaths, "subPaths");
            if (subPaths.Count == 0)
            {
                return root;
            }

            string text = subPaths[0];
            Item item = null;
            string decodedRequestedPart = MainUtil.DecodeText(text, MainUtil.TextReplacements);
            foreach (Item child in root.Children)
            {
                var candidateMatches = (bool)_candidateMatchesMethod.Invoke(this, new object[] { decodedRequestedPart, child });

                if (candidateMatches)
                {
                    List<string> range = subPaths.GetRange(1, subPaths.Count - 1);
                    Item item3 = ResolveRecursive(child, range);
                    if (item3 != null || _resolvingMode != MixedItemNameResolvingMode.DeepScan)
                    {
                        return item3;
                    }
                }
                else if (item == null && child.Name == "*")
                {
                    if (subPaths.Count == 1)
                    {
                        item = child;
                    }
                    else
                    {
                        var range = subPaths.GetRange(1, subPaths.Count - 1);
                        item = ResolveRecursive(child, range);
                    }
                }
            }
            return item;
        }
    }
}

Configure the new ItemResolver processor

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
  <sitecore>
    <pipelines>

      <httpRequestBegin>
        <processor patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" type="Drexel.Foundation.Framework.TempFix.DR1539.Pipelines.HttpRequest.ItemResolver, Drexel.Foundation.Framework" />
      </httpRequestBegin>
    </pipelines>

  </sitecore>
</configuration>

For what it’s worth, this behavior must have changed some time after Sitecore 7.5 because we are still running a Sitecore 7.5 instance where this issue doesn’t arise. I have submitted this as an issue to Sitecore and will update if/when they provide a solution or explain what I’m doing wrong.

Happy coding!

About Paul Martin

I enjoy rock climbing, playing guitar, writing code...
This entry was posted in Drexel, Sitecore and tagged . Bookmark the permalink.

Leave a comment