Our Sitecore Commerce catalog is fed by an import process where we typically import all of the data in a single import, which has worked OK up until now, but our client would like to be able to make small changes without having to go through the full import process. To facilitate this, we needed to ensure that minor edits (e.g. price change) from BizFX will appear on the website in a timely and well understood fashion. At first, we weren’t able to see the change reflected on the delivery server at all, so we started looking at clearing the Sitecore caches, but that still didn’t make the change appear without an app pool recycle, which is obviously not a good option when we get to a production environment. Knowing that the products refreshed OK when importing lots of them, we figured there had to be something going on under the hood that was preventing the change appearing.

Digging into the Sitecore Commerce code, I found this (from Sitecore.Commerce.Engine.Connect.dll, 9.0.2 version)

Sitecore.Commerce.Engine.Connect.Events.IndexingCompletedEventHandler .OnIndexingCompleted

if (completedEventArgs.SitecoreIds.Length <= 20)
  // ...
}
else
{
  // ...
  Log.Info("OnIndexingCompleted - Clear caches.", (object) this);
  CacheManager.ClearAllCaches();
  Log.Info("OnIndexingCompleted - Updating mapping entries.", (object) this);
  new CatalogRepository().UpdateMappingEntries(DateTime.UtcNow);
}

There’s the problem. After updating the index, it’s only going to drop the caches and reload the data if we’ve modified more than 20 items. (In later versions of Sitecore Commerce, this threshold is configurable via the configuration value FullCacheRefreshThreshold, so you could possibly set it to zero to solve this issue.)

To remediate the issue in this version, the simplest option within the existing structure is to add an additional handler after the existing one that will complete the product refresh and cache clear when less than 20 products have been changed.

Here’s the code to achieve that.

namespace MyNamespace.Feature.Catalog.EventHandlers
{
    public class IndexingCompletedEventHandler
    {
        public virtual void OnIndexingCompleted(object sender, EventArgs e)
        {
            var completedEventArgs = e as IndexingCompletedEventArgs;
            if (e == null || completedEventArgs == null)
            {
                return;
            }

            if (completedEventArgs.SitecoreIds.Length > 20)
            {
                return; //don't need to do anything, as this will be handled by the regular Commerce IndexingCompletedEventHandler because it's above the threshold.
            }

            var database = Factory.GetDatabase(completedEventArgs.DatabaseName, false);
            if (database == null)
            {
                return;
            }

            // only do this cache clear for web database.
            if (!database.Name.Equals("web", StringComparison.InvariantCultureIgnoreCase))
            {
                return;
            }
            
            var productTemplateId = ID.Parse("{225F8638-2611-4841-9B89-19A5440A1DA1}");
            var productVariantTemplateId = ID.Parse("{C92E6CD7-7F14-46E7-BBF5-29CE31262EF4}");
            var needToFetchCommerceProducts = false;

            // check whether any commerce products or variants have been changed.
            foreach (var id in completedEventArgs.SitecoreIds)
            {
                ID itemId;
                if (!ID.TryParse(id, out itemId))
                {
                    continue; //invalid item id.
                }

                // get the item from the database.
                var item = database.GetItem(itemId);
                if (item == null)
                {
                    continue; //couldn't find the item in the database.
                }

                // check whether the item is a product or a variant
                if (!item.TemplateID.Equals(productTemplateId) && !item.TemplateID.Equals(productVariantTemplateId))
                {
                    continue; //item is not a product or a variant
                }

                needToFetchCommerceProducts = true;
                break; //found one, no need to keep looking.
            }

            if (!needToFetchCommerceProducts)
            {
                Log.Info("No commerce products changed in index.", this);
                return;
            }
            
            // these are the caches that need to be dropped for the changes to be visible.
            var cacheNames = new[] { "CommerceCache.Default", "web[data]", "web[items]" };
            var allCaches = Sitecore.Caching.CacheManager.GetAllCaches();
            foreach (var cacheName in cacheNames)
            {
                var cache = allCaches.FirstOrDefault(x =>
                    x.Name.Equals(cacheName, StringComparison.InvariantCultureIgnoreCase));
                cache?.Clear();
                Log.Info($"Custom OnIndexingCompleted - cleared {cacheName} cache.", this);
            }
            Log.Info("Custom OnIndexingCompleted - Updating mapping entries.", this);
            new CatalogRepository().UpdateMappingEntries(DateTime.UtcNow);
        }
    }
}

And here’s the Sitecore configuration file.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <events>
            <event name="indexing:completed:remote">
                <handler type="MyNamespace.Feature.Catalog.EventHandlers.IndexingCompletedEventHandler,MyNamespace.Feature.Catalog" method="OnIndexingCompleted" />
            </event>
        </events>
    </sitecore>
</configuration>

I hope this has been helpful. Thanks for reading. :-)