Problems with urlResolver after upgrading to 12

Vote:
 

After a colleague of mine upgraded a client project from Optimizely 11 to 12, there's been quite a few bugs surfacing. However the one I want to ask about here is regarding the UrlResolver not working as it used to before which is causing issues.

In our SearchService and more specifically a QuickSearch, we do something like this:

public QuickSearchResultModel QuickSearch(string query, int articlesTake, int productsTake, int seriesTake)
{
    var client = SearchClient.Instance;
    var language = GetCurrentLanguage();

    var productsHelper = new ProductHelper();
    var productsNode = productsHelper.GetProductsNode();

    var articlesFieldSelectors = new (Expression<Func<ArticleVariation, string>>, double?)[] { (y => y.Code, null) };
    var articlesFieldSelectorsIEnumerable = new (Expression<Func<ArticleVariation, IEnumerable<string>>>, double?)[]
    {
        (y => y.ExtendedArticleData().SeriesNames, null)
    };

    var productsFieldSelectors = new (Expression<Func<ProductNode, string>>, double?)[] { (y => y.DisplayName, null) };
    var productsFieldSelectorsIEnumerable = new (Expression<Func<ProductNode, IEnumerable<string>>>, double?)[]
    {
        (y => y.ContentTags(), null),
        (y => y.ExtendedProductData().SeriesNames, null),
    };

    var seriesFieldSelectors = new (Expression<Func<SeriesNode, string>>, double?)[] { (y => y.DisplayName, null) };
    var seriesFieldSelectorsIEnumerable = new (Expression<Func<SeriesNode, IEnumerable<string>>>, double?)[]
    {
        (y => y.ContentTags(), null),
    };

    var trimmedQuery = GetTrimmedQuery(query);
    var isNumericQuery = query.All(char.IsDigit) || trimmedQuery.All(char.IsDigit);
    if (isNumericQuery)
    {
        query = trimmedQuery;
    }

    try
    {
        var result = client.MultiSearch<QuickSearchResultModelBase>()
        .Search<ArticleVariation, QuickSearchResultModelBase>(language, x => x.For(trimmedQuery)
                    .InField(y => y.Code)
                    .InField(y => y.ExtendedArticleData().SeriesNames)
                    .InFieldOldArticles(query)
                    .UsingSynonyms()
                    .WildcardSearch(query, articlesFieldSelectors, articlesFieldSelectorsIEnumerable)
                    .Filter(y => y.ExtendedArticleData().SeriesNames.Exists())
                    .CustomFilterForVisitor()
                    .Take(articlesTake)
                    .Cache()
                    .Select(y => new ArticleSearchResultItemModel
                    {
                        ArticleNumber = y.Code,
                        Image = y.ImageUrl(),
                        SeriesNames = y.ExtendedArticleData().SeriesNames,
                        SeriesList = JsonConvert.DeserializeObject<List<ArticleSeriesInfo>>(y.ExtendedArticleData().SeriesInfo),
                        OldArticleNumbers = y.OldArticles(),
                    }))

        .Search<ProductNode, QuickSearchResultModelBase>(language, x => x.For(query)
                    .InField(y => y.DisplayName)
                    .InField(Y => Y.ContentTags())
                    .InField(Y => Y.ExtendedProductData().SeriesNames)
                    .UsingSynonyms()
                    .WildcardSearch(query, productsFieldSelectors, productsFieldSelectorsIEnumerable)
                    .Filter(y => y.Ancestors().Match(productsNode.ContentLink.ToString()))
                    .FilterOrphans()
                    .FilterForVisitor()
                    .Take(productsTake)
                    .Cache()
                    .Select(y => new ProductSearchResultItemModel
                    {
                        Name = y.DisplayName,
                        Image = y.ImageUrl(),
                        USP = y.MainBody.AsCropped(100),
                        Url = _urlResolver.GetUrl(y.ContentLink, y.Language.Name)
                    }))

        .Search<SeriesNode, QuickSearchResultModelBase>(language, x => x.For(query)
                    .InField(y => y.DisplayName)
                    .InField(y => y.ContentTags())
                    .UsingSynonyms()
                    .WildcardSearch(query, seriesFieldSelectors, seriesFieldSelectorsIEnumerable)
                    .Filter(y => y.Ancestors().Match(productsNode.ContentLink.ToString()))
                    .Filter(y => y.IsHidden().Match(false))
                    .Filter(y => (y as NodeBase).SearchModel().Searchable.Match(true))
                    .FilterOrphans()
                    .FilterForVisitor()
                    .Take(seriesTake)
                    .Cache()
                    .Select(y => new SeriesSearchResultItemModel
                    {
                        Name = y.DisplayName,
                        Image = y.ImageUrl(),
                        USP = y.MainBody.AsCropped(100),
                        Url = y.SeriesUrl(),
                    }))
                .GetResult();
    }
    catch (Exception ex)
    {
        Log.Error("Error", ex);
    }
}

In the first search of the multisearch, where we search for an article (more specifically the part I point at below).

.Select(y => new ArticleSearchResultItemModel
{
  ArticleNumber = y.Code,
  Image = y.ImageUrl(),
  SeriesNames = y.ExtendedArticleData().SeriesNames,
  SeriesList = JsonConvert.DeserializeObject<List<ArticleSeriesInfo>>(y.ExtendedArticleData().SeriesInfo), <-- this part specifically
  OldArticleNumbers = y.OldArticles(),
}))

We go into an extension method for the ArticleVariation with ExtendedArticleData() that looks like this:

        private static readonly IContentLoader _contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
        private static readonly IUrlResolver _urlResolver = ServiceLocator.Current.GetInstance<IUrlResolver>();
        private static readonly IRelationRepository _relationRepository = ServiceLocator.Current.GetInstance<IRelationRepository>();
        private static readonly IPublishedStateAssessor _publishedStateAssessor = ServiceLocator.Current.GetInstance<IPublishedStateAssessor>();
        private static readonly IContentService _contentService = ServiceLocator.Current.GetInstance<IContentService>();
        private static readonly IAssociationRepository _associationRepository = ServiceLocator.Current.GetInstance<IAssociationRepository>();

        public static ExtendedArticleVariationData ExtendedArticleData(this ArticleVariation article)
        {
            var series = article.Series();
            var seriesUrl = series.FirstOrDefault().SeriesUrl();

            return new ExtendedArticleVariationData
            {
                SeriesNames = series.Select(x => x.DisplayName),
                SeriesInfo = article.GetArticleSeriesInfo(series)
            };
        }

        public static IEnumerable<SeriesNode> Series(this ArticleVariation article)
        {
            try
            {
                List<SeriesNode> seriesNodes = new List<SeriesNode>();

                var productHelper = new ProductHelper();
                var productsNode = productHelper.GetProductsNode();
                var orphanContainer = productHelper.GetOrphanContainer();

                var parentRefs = _relationRepository.GetParents<Relation>(article.ContentLink)?.Select(x => x.Parent);

                if (parentRefs != null && parentRefs.Any())
                {
                    var parents = _contentLoader.GetItems(parentRefs, article.Language).OfType<SeriesNode>().Where(_publishedStateAssessor.IsPublished);

                    foreach (var parent in parents)
                    {
                        var seriesAncestors = _contentLoader.GetAncestors(parent.ContentLink);

                        if (seriesAncestors.Any(x => x.ContentLink.ID.Equals(productsNode.ContentLink.ID)) && !seriesAncestors.Any(x => x.ContentLink.ID.Equals(orphanContainer.ContentLink.ID)))
                        {
                            seriesNodes.Add(parent);
                        }
                    }
                }

                return seriesNodes;
            }
            catch (Exception ex)
            {
                Log.Error("Something went wrong", ex);
            }

            return new List<SeriesNode>();
        }

        public static string GetArticleSeriesInfo(this ArticleVariation article, IEnumerable<SeriesNode> seriesNodes)
        {
            try
            {
                var articleSeries = new List<ArticleSeriesInfo>();

                foreach (var seriesNode in seriesNodes)
                {
                    articleSeries.Add(new ArticleSeriesInfo { SeriesName = seriesNode.DisplayName, ArticleUrl = GetArticleUrl(seriesNode, article) });
                }

                return JsonConvert.SerializeObject(articleSeries);
            }
            catch (Exception ex)
            {
                Log.Error("Something went wrong", ex);
            }

            return string.Empty;
        }

        public static string GetArticleUrl(this ArticleVariation article)
        {
            var firstSeries = article.Series().FirstOrDefault();
            if (firstSeries != null)
            {
                return GetArticleUrl(firstSeries, article);
            }

            return "#";
        }

        private static string GetArticleUrl(SeriesNode series, ArticleVariation article)
        {
            var product = _contentLoader.Get<ProductNode>(series.ParentLink, article.Language);
            var productUrl = _urlResolver.GetUrl(product.ContentLink, product.Language.Name);

            return string.Format($"{productUrl}?code={product.Code}&filters=SeriesCodes%3DSeriesCodes%3a{series.Code}&am={article.Code}#parts");
        }

        public class ExtendedArticleVariationData
        {
            public IEnumerable<string> SeriesNames { get; set; }
            public string SeriesInfo { get; set; }
        }
    }

Now to the issue, if you follow the trail from the QuickSearch into the ExtendedArticleData you will eventually end up at this code snippet:

private static string GetArticleUrl(SeriesNode series, ArticleVariation article)
{
  var product = _contentLoader.Get<ProductNode>(series.ParentLink, article.Language);
  var productUrl = _urlResolver.GetUrl(product.ContentLink, product.Language.Name); <-- this part
  
  return string.Format($"{productUrl}?code={product.Code}&filters=SeriesCodes%3DSeriesCodes%3a{series.Code}&am={article.Code}#parts");
}

For some reason the urlResolver here always returns null. The product variable seems legit, and provides the content link and "en" in this case as the language, but it still throws null.

This happens when clearing the search index, and running a new "Search & Navigation Content Indexing Job", and it only starts returning something other than null if I locate that specific product in episerver and save it. But then, when re-indexing, it stops working all over again. The issue was the same with the product part of the multi search, but in that case I was able to solve it by moving the _urlresolver.GetUrl outside of the extension and do it directly as you can also see up in the MultiSearch. 

Does anyone know what might cause this? Do I need to do something different with the UrlResolver? This was not an issue before upgrading to 12 and the code here is basically the same.

Version information
CMS: 12.15.0
Commerce: 14.9.0
Search & Navigation: 14.2.4.0

#298413
Edited, Mar 16, 2023 7:55
Vote:
 

Do you have a wildcard mapping in site settings hosts? Note that there is no http context when the indexing job runs, so for the UrlResolver to function correctly it needs a host to fall back on. There is however an http context during the event-based indexing, i.e. when you edit content.

#298494
Mar 17, 2023 15:27
sebp - Mar 20, 2023 8:22
Thank you for the information, I've checked and we do have a wildcard setup so this should not be an issue :)
Vote:
 

Creating my own indexing job for commerce solves the issue, hence the issue seems to be connected to the built in indexing job for some reason.

#298915
Edited, Mar 24, 2023 12:43
Eric Herlitz - Mar 24, 2023 12:53
It's good practice to do your own indexing :)
sebp - Mar 24, 2023 14:26
Yes, definitely, I was just not expecting this to be the cause of the issue :D
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.