Commerce Connect – How to synchronize categories (classifications) from ECS to Sitecore

Here is a quick how to guide for implementing a one-way synchronization of categories from an ECS (External Commerce System) into Sitecore, and a follow up to Dan Schoenberg’s post on syncing divisions.

Note: This guide was written while using Commerce Connect 7.2/7.5

Step 1: Update configuration settings for 1-way synchronization
To support a one-way synchronization of categories where data from your ECS is populated in Sitecore upon a Sync operation, make the following configuration changes. Note, I wanted to avoid making changes to the default commerce connect configuration files, so I made my changes in a separate configuration file, and that’s why I used the delete patch approach.

<commerce.synchronizeProducts.synchronizeClassifications>
  <processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.ReadSitecoreClassifications, Sitecore.Commerce">
    <patch:delete />
  </processor>
  <processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.ResolveClassificationsChanges, Sitecore.Commerce">
    <patch:delete />
  </processor>
  <processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.SaveClassificationsToExternalCommerceSystem, Sitecore.Commerce">
    <patch:delete />
  </processor>
</commerce.synchronizeProducts.synchronizeClassifications>

Step 2: Create custom classification synchronization processors
When using Commerce Connect, a category structure will typically be contained within a ClassificationGroup, and your categories will be setup as a Classification hierarchy. To demonstrate both of these concepts, I put together basic code to setup custom processors for both the ClassificationGroup and the Classifications. There are a few interesting things to make note of when reviewing the code.

Using the Request object from the given ServicePipelineArgs will allow you to retrieve a current collection of data keyed by property name in the pipeline and act upon it. If you get an empty collection, you should create the collection and assign that to the Request object which will all the other processors in the in the pipeline to act upon the data accordingly.

The ClassificationGroup processor keys off of the “ClassificationGroups” property name and the Classification processor keys off of the “Classification” property name.

When setting up your categories (Classifications) you’ll want to assign them to a ClassificationGroup. To make this work, you will need to assign the collection of categories (Classifications) to an instance of a ClassificationGroup as demonstrated in lines 58-63.

In order to create your category hierarchy, simply use the ExternalParentId property on the Classification object; as in the mock data that was setup in lines 76-97. This example only shows one level deep but the nesting has no foreseeable limitations.

Ok, here’s the code:

public abstract class AbstractSyncProcessor : PipelineProcessor<ServicePipelineArgs>
{
	protected List<T> ExtractDataCollection<T>(ServicePipelineArgs args, string propertyIdentifier)
	{
		List<T> collection = args.Request.Properties[propertyIdentifier] as List<T>;
		if (collection == null)
		{
			collection = new List<T>();
			args.Request.Properties[propertyIdentifier] = collection;
		}

		return collection;
	}
}

public class ClassificationGroupSyncProcessor : AbstractSyncProcessor
{
	public static string ClassificationGroupPropertyName = "ClassificationGroups";

	public ClassificationGroupSyncProcessor()
	{ }

	public override void Process(ServicePipelineArgs args)
	{
		try
		{
			List<ClassificationGroup> groups = ExtractDataCollection<ClassificationGroup>(args, ClassificationGroupPropertyName);
			ProcessMockData(groups);
			args.Result.Success &= true;
		}
		catch (Exception e)
		{
			args.Result.Success = false;
			throw e;
		}
	}

	public void ProcessMockData(List<ClassificationGroup> groups)
	{
		groups.Add(new ClassificationGroup { Name = "Launch Sitecore Taxonomy", ExternalId = "LaunchSitecoreTaxonomy-CG", Description = "A default category structure for organizing product information." });
	}
}

public class ClassificationSyncProcessor : AbstractSyncProcessor
{
	public static string ClassificationPropertyName = "Classification";

	public ClassificationSyncProcessor()
	{ }

	public override void Process(ServicePipelineArgs args)
	{
		try
		{
			List<Classification> classifications = ExtractDataCollection<Classification>(args, ClassificationPropertyName);
			ProcessMockData(classifications);

			List<ClassificationGroup> groups = ExtractDataCollection<ClassificationGroup>(args, ClassificationGroupSyncProcessor.ClassificationGroupPropertyName);
			foreach(ClassificationGroup group in groups)
			{
				if (group.ExternalId == "LaunchSitecoreTaxonomy-CG")
					group.Classifications = new ReadOnlyCollection<Classification>(classifications);
			}

			args.Result.Success &= true;
		}
		catch (Exception e)
		{
			args.Result.Success = false;
			throw e;
		}
	}

	public void ProcessMockData(List<Classification> classifications)
	{
		classifications.Add(new Classification { Name = "Sitecore Modules", ExternalId = "SitecoreModules-CAT", ExternalParentId = null });
		classifications.Add(new Classification { Name = "Developer Accelerators", ExternalId = "DeveloperAccelerators-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Administration", ExternalId = "Administration-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Client Features", ExternalId = "ClientFeatures-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Visitor Interaction", ExternalId = "VisitorInteraction-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Translation and Localization", ExternalId = "TranslationAndLocalization-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Media", ExternalId = "Media-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Website Optimization", ExternalId = "WebsiteOptimization-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Portlets", ExternalId = "Portlets-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Reporting", ExternalId = "Reporting-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Search", ExternalId = "Search-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Setup and Deployment", ExternalId = "SetupAndDeployment-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Social and Community", ExternalId = "SocialAndCommunity-CAT", ExternalParentId = "SitecoreModules-CAT" });
		classifications.Add(new Classification { Name = "Webmaster Tools", ExternalId = "WebmasterTools-CAT", ExternalParentId = "SitecoreModules-CAT" });

		classifications.Add(new Classification { Name = "Sitecore Classes and Training", ExternalId = "SitecoreClassesAndTraining-CAT", ExternalParentId = null });
		classifications.Add(new Classification { Name = "Content Editor", ExternalId = "ContentEditor-CAT", ExternalParentId = "SitecoreClassesAndTraining-CAT" });
		classifications.Add(new Classification { Name = "Page Editor", ExternalId = "PageEditor-CAT", ExternalParentId = "SitecoreClassesAndTraining-CAT" });
		classifications.Add(new Classification { Name = "Personalization", ExternalId = "Personalization-CAT", ExternalParentId = "SitecoreClassesAndTraining-CAT" });
		classifications.Add(new Classification { Name = "Engagement Plans", ExternalId = "EngagementPlans-CAT", ExternalParentId = "SitecoreClassesAndTraining-CAT" });
		classifications.Add(new Classification { Name = "Visitor Profiles", ExternalId = "VisitorProfiles-CAT", ExternalParentId = "SitecoreClassesAndTraining-CAT" });
		classifications.Add(new Classification { Name = "Campaigns", ExternalId = "Campaigns-CAT", ExternalParentId = "SitecoreClassesAndTraining-CAT" });
	}
}

Step 3: Patch in processor configurations for custom classification synchronization processors Add the custom synchronization processors into the pipeline via configuration updates below the settings you made in step 1. After your changes your final configuration should like something similar to this. Make sure that in your final configuration that the ClassificationGroup processor runs before the Classification processor or you could face issues with your expected data not being present in the Request object.

<commerce.synchronizeProducts.synchronizeClassifications>
  <processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.ReadSitecoreClassifications, Sitecore.Commerce">
    <patch:delete />
  </processor>
  <processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.ResolveClassificationsChanges, Sitecore.Commerce">
    <patch:delete />
  </processor>
  <processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.SaveClassificationsToExternalCommerceSystem, Sitecore.Commerce">
    <patch:delete />
  </processor>
  <!-- Setup a custom processor to handle synchronization of Mock Data for Classifications -->
  <processor type="YourNamespaceHere.ClassificationSyncProcessor, YourLibraryNameHere" patch:after="processor[@type='Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.ReadExternalCommerceSystemClassifications, Sitecore.Commerce']" />
  <processor type="YourNamespaceHere.ClassificationGroupSyncProcessor, YourLibraryNameHere" patch:after="processor[@type='Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.ReadExternalCommerceSystemClassifications, Sitecore.Commerce']" />
</commerce.synchronizeProducts.synchronizeClassifications>

Step 4: Synchronize the Product Repository
Build your solution and “Synchronize artifacts” on your product repository: .

CommerceConnect-Sync

Once complete, your categories will be populated into your Commerce Connect product repository in Sitecore with results similar to this:

CommerceConnect-MockCategories
Advertisements

Fix for index not found error while installing Commerce Connect 7.2

I found a missing step in the Sitecore Commerce Connect 7.2 Installation Guide that I’d like to share:

On step 5 of the “Integrating an External Commerce System – Creating a Product Repository” guide you are instructed to Synchronize All Products, however when I reached this step I received the following error:

SynchronizeError

Upon further inspection in the Sitecore log file, I found the following:

Exception: System.Reflection.TargetInvocationException
Message: Exception has been thrown by the target of an invocation.
Source: mscorlib
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.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
at (Object , Object[] )
at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
at Sitecore.Jobs.Job.ThreadEntry(Object state)

Nested Exception

Exception: System.ArgumentException
Message: Index commerce_products_master_index was not found
Source: Sitecore.ContentSearch
at Sitecore.ContentSearch.ContentSearchManager.GetIndex(String name)
at Sitecore.Commerce.Data.Products.ProductRepository.GetEntityItems(Item repositoryItem)
at Sitecore.Commerce.Data.Products.ArtifactRepository`1.GetEntities(Item root)
at Sitecore.Commerce.Pipelines.Products.GetSitecoreProductList.GetSitecoreProductList.Process(ServicePipelineArgs args)
at (Object , Object[] )
at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
at (Object , Object[] )
at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
at Sitecore.Commerce.Services.ServiceProvider.RunPipeline[TRequest,TResult](String pipelineName, TRequest request)
at Sitecore.Commerce.Services.Products.ProductSynchronizationProvider.RunPipelineBody[TRequest,TResult](String pipelineName, TRequest request)
at Sitecore.Commerce.Services.Products.ProductSynchronizationProvider.SynchronizeProducts(SynchronizationRequest request)

There is an easy fix for this, simply enable your “Sitecore.Commerce.Products.Lucene.Index.Master.config” file and verify all of its settings.

IndexConfigSetup

Once completed, re-run the product synchronization tool and you can continue with the rest of your Sitecore Commerce Connect 7.2 installation.