Find indexing job + HierarchicalCatalogPartialRouter: A note

I ran into this problem recently and while in the end it’s quite simple issue (Everything is simple if we understand it, right?), it costed me quite many hairs in the process – as it involved debugging with 3 solutions – Find.Commerce (where the problem appears), Commerce (where the router does the work), CMS Core (where the routers are handled). It was both fun, and confusing.

The problem as a customer has this code in an initialization module:

            var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
            var referenceConverter = ServiceLocator.Current.GetInstance<ReferenceConverter>();

            var firstCatalog = contentLoader.GetChildren<CatalogContent>(referenceConverter.GetRootLink()).FirstOrDefault();

            var partialRouter = new HierarchicalCatalogPartialRouter(() => SiteDefinition.Current.StartPage, firstCatalog, false);

            routes.RegisterPartialRouter(partialRouter);

Nothing is special here – this is a technique to remove the catalog name from the partial router url. If you use CatalogRouteHelper.MapDefaultHierarchialRouter, then the url will be something like this

http://commerce/en/fashion/mens/mens-shoes/p-36127195/

where “fashion” is your catalog name. It’s fine, but you might want it to be

http://commerce/en/mens/mens-shoes/p-36127195/

only. If you have only one catalog and you don’t want to expose it/or you want shorter url – why not? The snippet above solves that problem. But only if you are not using Find – and the Find indexing job.

If you try to run the Find content indexing job with the code above, then your log file will be filled with errors like this:

System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ArgumentNullException: The provided content link does not have a value.
Parameter name: contentLink
   at EPiServer.Core.Internal.DefaultContentLoader.Get[T](ContentReference contentLink, LoaderOptions loaderOptions)
   at EPiServer.Core.Internal.DefaultContentLoader.Get[T](ContentReference contentLink)
   at EPiServer.Web.Routing.Segments.Internal.NodeSegment.GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values)
   at EPiServer.Web.Routing.Segments.SegmentBase.GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values, HashSet`1 usedValues)
   at EPiServer.Web.Routing.Segments.Internal.NodeSegment.GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values, HashSet`1 usedValues)
   at EPiServer.Web.Routing.Internal.DefaultContentRoute.AddVirtualPathFromSegments(StringBuilder virtualPath, RequestContext requestContext, RouteValueDictionary values, HashSet`1 usedValues, Int32 lastNonDefaultIndex)
   at EPiServer.Web.Routing.Internal.DefaultContentRoute.GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
   at EPiServer.Web.Routing.Internal.DefaultUrlResolver.GetUrlFromRoute(ContentReference contentReference, String language, RouteValueDictionary routeValues, RequestContext requestContext)
   at EPiServer.Web.Routing.Internal.DefaultUrlResolver.GetVirtualPath(ContentReference contentLink, String language, VirtualPathArguments arguments)
   at EPiServer.Web.Routing.UrlResolver.GetUrl(ContentReference contentLink, String language)
   at EPiServer.Web.Routing.UrlResolver.GetUrl(ContentReference contentLink)
   at EPiServer.Find.Commerce.CommerceUnifiedSearchSetUp.GetContentUrl(ContentReference contentLink) in C:\episerver\findcommerce\EPiServer.Find.Commerce\CommerceUnifiedSearchSetUp.cs:line 143
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at System.Delegate.DynamicInvokeImpl(Object[] args)
   at EPiServer.Find.UnifiedSearch.IndexProjection.GetUrl(Object o)
   at EPiServer.Find.DelegateValueProvider`2.GetValue(Object target)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CalculatePropertyValues(JsonWriter writer, Object value, JsonContainerContract contract, JsonProperty member, JsonProperty property, JsonContract& memberContract, Object& memberValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
   at EPiServer.Find.Api.BulkActionConverter.WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeConvertable(JsonWriter writer, JsonConverter converter, Object value, JsonContract contract, JsonContainerContract collectionContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
   at Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
   at EPiServer.Find.Json.Serializer.SerializeToTextWriter(JsonSerializer serializer, Object value, TextWriter textWriter)
   at EPiServer.Find.Json.Serializer.SerializeObjectsToJsonRequest(JsonSerializer serializer, IJsonRequest jsonRequest, IEnumerable values)
   at EPiServer.Find.Api.BulkCommand.Execute()
   at EPiServer.Find.Cms.ContentIndexer.IndexWithRetry(IContent[] contents, Int32 maxRetries)
   at EPiServer.Find.Cms.ContentIndexer.Index(IEnumerable`1 content, IndexOptions options)
   at EPiServer.Find.Cms.ContentIndexer.IndexBatch(IEnumerable`1 content, Action`1 statusAction, Int32& numberOfContentErrors, Int32& indexingCount)

So what’s wrong?

At first sight, it seems that the wildcard mapping was not added. But it was there. I was pulling my hairs (and quite hard, actually) until I look into the scheduled job itself. It was running with all the SiteDefinitionplus SiteDefinition.Empty.

The reason was SiteDefinition.Empty was needed, for indexing global assets and other data. However, SiteDefinition.Empty.StartPage is an empty ContentReference, which is assigned to be the BasePathRoot of the returned PartialRouteData, which will mess up with IContentLoader.Get when it tries to load that content.

You might argue that as a bug. Maybe. But the fix is easy enough for you to do it in your implementation. Simply change this line

var partialRouter = new HierarchicalCatalogPartialRouter(
() => SiteDefinition.Current.StartPage, firstCatalog, false);

to

var partialRouter = new HierarchicalCatalogPartialRouter(
() => ContentReference.IsNullOrEmpty(SiteDefinition.Current.StartPage) ? 
SiteDefinition.Current.RootPage : SiteDefinition.Current.StartPage, firstCatalog, false);

i.e. fall back to the RootPage if the StartPage is empty. This will only be used in Find content indexing job, and just to make the content loader happy.

CatalogRouteHelper.MapDefaultHierarchialRouter use the same technique internally, that’s why if you are using the default hierarchical router, all is well!

Leave a Reply

Your email address will not be published. Required fields are marked *