<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Calin Lupas</title><link href="http://world.optimizely.com" /><updated>2020-08-16T22:46:17.0000000Z</updated><id>https://world.optimizely.com/blogs/calin-lupas/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Episerver Commerce - Catalog Low-level APIs - Improve the performance when importing thousands of product variants in bulk</title><link href="https://world.optimizely.com/blogs/calin-lupas/dates/2020/8/episerver-commerce---catalog-low-level-api---improve-the-performance-when-importing-thousands-of-product-variants/" /><id>&lt;p&gt;The proposed implementation aims to offer a solution for importing thousands of product variants in a performant way using catalog low-level APIs.&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;Introduction&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;In our projects, we&amp;rsquo;ve built a connector between Dynamics 365 and Episerver to synchronize the online store and the published catalogs. The D365-Episerver connector, among other functionalities, has few Episerver scheduled jobs to import categories, products and variants from D365 into Episerver.&lt;/p&gt;
&lt;p&gt;We are using the Catalog content APIs to synchronize and import catalogs, categories, products, variants, related entries, prices, inventories, images and more.&lt;/p&gt;
&lt;p&gt;For one of the projects, we were facing a performance issue to import the catalog due to the number of variants that a product can have.&lt;/p&gt;
&lt;p&gt;Some products have 100, 500, 1K, 2K, 3K, 4K, 5K, 6K and even 12+K variants for a total of ~ &lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;100 000 variants in the entire catalog&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Using the Catalog content APIs for variants, the sync products job always failed with timeout exceptions and took in some cases more than 9 hours.&lt;/p&gt;
&lt;p&gt;As you can imagine, we had to find a solution to import all these variants in a fast and performant way to reduce the synchronization time.&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;Solution&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Leverage Episerver Commerce - Catalog Low-level APIs to import thousands of product variants in bulk to minimize the synchronization and import time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;Sync a full online catalog from D365 into Episerver in less than 1.5h (25 categories, 83 products and 99 201 variants in 85 minutes)&lt;/span&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/671beca56a5246efbdefd9a3a35cc78b.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This blog post describes in detail the complete solution to import and bulk save thousands of variants using low-level APIs.&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;How it works&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;An Episerver scheduled job &amp;ldquo;&lt;em&gt;Sync All Products from D365 into Episerver&lt;/em&gt;&amp;rdquo; is syncing the published Catalogs of the Online Store from Dynamics 365 into Episerver. The sync includes the catalogs, the categories of each catalog, the products of each categories and the variants for each products.&lt;/p&gt;
&lt;p&gt;The current solution describes the implementation we are using to bulk save product variants.&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;Implementation&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s go into detail and explain the solution from a code implementation perspective. It consists of:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;data models&lt;/strong&gt; - &lt;code&gt;BaseDTO.cs&lt;/code&gt; and &lt;code&gt;VariantDTO.cs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;service interface&lt;/strong&gt; &amp;ndash; &lt;code&gt;IVariantIntegrationService.cs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;service implementation&lt;/strong&gt; &amp;ndash; &lt;code&gt;VariantIntegrationService.cs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;scheduled job&lt;/strong&gt; caller method - &lt;code&gt;D365SyncProductsScheduledJob.cs&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;1. Data Models&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The base DTO model for all the sync models defines the base properties and a dictionary of key=value properties used to sync the content properties:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class BaseDTO
{
	public int? Id { get; set; }

	public string Code { get; set; }

	public string Name { get; set; }

	public string DisplayName { get; set; }

	public string Description { get; set; }

	public string Language { get; set; }

	// The key is the property name
	// The value is the property value
	public Dictionary&amp;lt;string, object&amp;gt; Properties { get; set; }

	public object SourceObject { get; set; }

	public string GetJsonSourceObject()
	{
		if (SourceObject == null)
			return string.Empty;

		return JObject
					.FromObject(SourceObject)
					.ToString();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The variant DTO model has the following properties:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class VariantDTO : BaseDTO
{
	public WebCategory Category { get; set; }

	public WebProduct Product { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;WebCategory&lt;/code&gt; and the &lt;code&gt;WebProduct&lt;/code&gt; types are simply the catalog content types that we are using for the &lt;code&gt;NodeContent&lt;/code&gt; and &lt;code&gt;ProductContent&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The result DTO model has the following implementation:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class ResultModelDTO
{
	private List&amp;lt;Exception&amp;gt; _exceptions = null;

	public IEnumerable&amp;lt;Exception&amp;gt; Exceptions
	{
		get
		{
			if (_exceptions != null)
				return _exceptions;

			return Enumerable.Empty&amp;lt;Exception&amp;gt;();
		}
	}

	public bool IsSuccess
	{
		get
		{
			return (_exceptions == null || !_exceptions.Any());
		}
	}

	public bool IsFail =&amp;gt; !IsSuccess;

	public void AddException(Exception exc)
	{
		if (exc == null)
			return;

		_exceptions = _exceptions ?? new List&amp;lt;Exception&amp;gt;();
		_exceptions.Add(exc);
	}

	public void AddError(string errorMessage)
	{
		AddException(new ApplicationException(errorMessage));
	}

	public void Fail(string errorMessage)
	{
		if (IsFail)
			return;

		AddException(new ApplicationException(errorMessage));
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;2. Service Interface - IVariantIntegrationService.cs&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;We have an integration library that is responsible for importing data into Episerver Commerce using the Episerver APIs.&lt;/p&gt;
&lt;p&gt;It acts as a wrapper library on top of the Episerver APIs and it is used by other libraries to accomplish our integration scenarios.&lt;/p&gt;
&lt;p&gt;The variant interface defines the following method pertaining to bulk import variants (&lt;em&gt;the other methods exposed are not relevant to this solution&lt;/em&gt;):&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public interface IVariantIntegrationService
{
   ...

   ResultModelDTO SaveBulk(IList&amp;lt;VariantDTO&amp;gt; models);

   ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;3. Service Implementation &amp;ndash; VariantIntegrationService.cs&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The integration library contains a service implementation for the above variant interface and the save bulk method (&lt;em&gt;additional code not related to his solution has been removed&lt;/em&gt;):&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class VariantIntegrationService : IVariantIntegrationService
{
	#region Members
   
	private readonly CatalogMetaObjectRepository _catalogMetaObjectRepository;
	private readonly int _batchSize = 1000;
	private static readonly ILogger Logger = LogManager.GetLogger();

	#endregion

	#region Constructors

	public VariantIntegrationService(         
		CatalogMetaObjectRepository catalogMetaObjectRepository
		) 
	{   
		_catalogMetaObjectRepository = catalogMetaObjectRepository;
	}

	#endregion

	#region IVariantIntegrationService
	
	public ResultModelDTO SaveBulk(IList&amp;lt;VariantDTO&amp;gt; models)
	{
		var result = new ResultModelDTO();

		var category = models.FirstOrDefault()?.Category;
		var product = models.FirstOrDefault()?.Product;
		
		if (category == null || product == null)
		{
			result.Fail(&quot;SaveBulk - Variants - Category or product models are empty.&quot;);
			return result;
		}

		var catalogId = product.CatalogId;
		var startDate = DateTime.UtcNow;
		var endDate = DateTime.UtcNow.AddYears(10);

		MetaClass mcsku = MetaClass.Load(CatalogContext.MetaDataContext, &quot;WebVariant&quot;);
		int skuMetaClassId = mcsku.Id;

		var catalogContextCurrent = CatalogContext.Current;

		var catalogDto = catalogContextCurrent.GetCatalogDto(catalogId, new CatalogResponseGroup(CatalogResponseGroup.ResponseGroup.CatalogInfo));

		if (catalogDto.Catalog.Count &amp;gt; 0)
		{
			var productEntry = catalogContextCurrent.GetCatalogEntryDto(product.Code, new CatalogEntryResponseGroup(CatalogEntryResponseGroup.ResponseGroup.Variations));

			if (productEntry == null || productEntry.CatalogEntry.Count == 0)
			{
				result.Fail(&quot;SaveBulk - Variants - Product entry is empty.&quot;);
				return result;
			}

			// Step 1: Create all variants
			foreach (var variantDto in models)
			{
				var variantEntryRow = productEntry.CatalogEntry.FirstOrDefault(i =&amp;gt; i.Code == variantDto.Code);

				if (variantEntryRow == null)
				{
					var variantCatalogEntryDto = catalogContextCurrent.GetCatalogEntryDto(variantDto.Code);

					variantEntryRow = (variantCatalogEntryDto.CatalogEntry != null &amp;amp;&amp;amp; variantCatalogEntryDto.CatalogEntry.Count &amp;gt; 0)
									  ? variantCatalogEntryDto.CatalogEntry[0]
									  : null;
				}

				if (variantEntryRow == null)
				{
					variantEntryRow = productEntry.CatalogEntry.NewCatalogEntryRow();
					variantEntryRow.CatalogId = catalogId;
					variantEntryRow.ClassTypeId = &quot;Variation&quot;;
					variantEntryRow.Code = variantDto.Code;
					variantEntryRow.MetaClassId = skuMetaClassId;
					variantEntryRow.IsActive = true;
					variantEntryRow.Name = variantDto.Name;
					variantEntryRow.StartDate = startDate;
					variantEntryRow.EndDate = endDate;

					if (variantEntryRow.RowState == DataRowState.Detached)
						productEntry.CatalogEntry.AddCatalogEntryRow(variantEntryRow);

					var newVariationRow = productEntry.Variation.NewVariationRow();

					newVariationRow.SetMerchantIdNull();
					newVariationRow.SetListPriceNull();
					newVariationRow.TaxCategoryId = 0;
					newVariationRow.TrackInventory = false;
					newVariationRow.PackageId = 0;
					newVariationRow.WarehouseId = 0;
					newVariationRow.Weight = 0;
					newVariationRow.Height = 0;
					newVariationRow.Length = 0;
					newVariationRow.Width = 0;
					newVariationRow.MinQuantity = 0;
					newVariationRow.MaxQuantity = 0;
					newVariationRow.CatalogEntryId = variantEntryRow.CatalogEntryId;

					if (newVariationRow.RowState == DataRowState.Detached)
						productEntry.Variation.AddVariationRow(newVariationRow);
				}
				else
				{
					variantEntryRow.IsActive = true;
					variantEntryRow.Name = variantDto.Name;
					variantEntryRow.StartDate = startDate;
					variantEntryRow.EndDate = endDate;
				}

				variantDto.Id = variantEntryRow.CatalogEntryId;
			}

			// Step 2 : Save product variants changes
			try
			{
				if (productEntry.HasChanges())
					SaveCatalogEntryDtoByBatch(productEntry);

				// Once saved by batches, set the variantDto.Id
				if (productEntry.CatalogEntry.Count &amp;gt; _batchSize 
					|| models.Any(i =&amp;gt; !i.Id.HasValue || i.Id.GetValueOrDefault() &amp;lt;= 0))
				{
					productEntry = catalogContextCurrent.GetCatalogEntryDto(product.Code, new CatalogEntryResponseGroup(CatalogEntryResponseGroup.ResponseGroup.Variations));

					Parallel.ForEach(models.Where(i =&amp;gt; !i.Id.HasValue || i.Id.GetValueOrDefault() &amp;lt;= 0), variantDto =&amp;gt;
					{
						variantDto.Id = productEntry.CatalogEntry.FirstOrDefault(i =&amp;gt; i.Code == variantDto.Code)?.CatalogEntryId;

						if (!variantDto.Id.HasValue)
						{
							variantDto.Id = catalogContextCurrent.GetCatalogEntryDto(variantDto.Code)?.CatalogEntry[0]?.CatalogEntryId;
						}
					});
				}
			}
			catch (Exception ex)
			{
				var error = $&quot;VariantIntegrationService - Save Bulk - Error when saving product entry with code = {product.Code}&quot;;
				Logger.Error(error, ex);

				result.AddError($&quot;{error} : {ex.Message}&quot;);
				return result;
			}
			
			// Step 3: Save variant properties (meta objects)
			var language = product.Language.Name;
			var identityName = Thread.CurrentPrincipal.Identity != null ? Thread.CurrentPrincipal.Identity.Name : string.Empty;
			var metaDataContext = CreateMetaDataContext(language);
			var variantMetaObjects = new List&amp;lt;MetaObject&amp;gt;();

			foreach (var variantDto in models)
			{
				if (!variantDto.Id.HasValue)
					continue;

				var metaObject = _catalogMetaObjectRepository.Load(variantDto.Id.Value, skuMetaClassId, language, ReadMode.UnCached);

				if (metaObject == null)
				{
					metaObject = new MetaObject(mcsku, variantDto.Id.Value)
					{
						CreatorId = identityName,
						ModifierId = identityName
					};
				}

				metaObject[&quot;DisplayName&quot;] = variantDto.DisplayName;
				metaObject[&quot;Description&quot;] = variantDto.Description;
				metaObject[&quot;JsonSourceData&quot;] = variantDto.GetJsonSourceObject();

				metaObject[&quot;Epi_IsPublished&quot;] = true;
				metaObject[&quot;Epi_StartPublish&quot;] = startDate;
				metaObject[&quot;Epi_StopPublish&quot;] = endDate;

				metaObject.SetProperties(variantDto);

				variantMetaObjects.Add(metaObject);
			}

			try
			{
				SaveCatalogMetaObjectsByBatch(language, variantMetaObjects);
			}
			catch (Exception ex)
			{
				Logger.Error($&quot;VariantIntegrationService - Save Bulk - Error when saving meta objects for product code = {product.Code}: &quot;, ex);

				// Update meta object 1 by 1
				foreach (var metaObject in variantMetaObjects)
				{
					try
					{
						_catalogMetaObjectRepository.Update(metaObject, language);
					}
					catch (Exception exc)
					{
						var error = $&quot;VariantIntegrationService - Save Bulk - Error when saving meta object for product code = {product.Code}&quot;;
						Logger.Error(error, exc);

						result.AddError($&quot;{error} : {exc.Message}&quot;);
						return result;
					}
				}
			}
			
			// Step 4: Create all relations (variant - category AND product - variant)
			var categoryEntry = catalogContextCurrent.GetCatalogNodeDto(category.Code);

			if (categoryEntry == null || categoryEntry.CatalogNode.Count == 0)
			{
				result.Fail(&quot;SaveBulk - Variants - Category entry is empty.&quot;);
				return result;
			}

			var categoryRelation = catalogContextCurrent.GetCatalogRelationDto(catalogId, 
				categoryEntry.CatalogNode[0].CatalogNodeId, 0, &quot;&quot;, 
				new CatalogRelationResponseGroup(CatalogRelationResponseGroup.ResponseGroup.NodeEntry));

			var productRelation = catalogContextCurrent.GetCatalogRelationDto(catalogId, 
				categoryEntry.CatalogNode[0].CatalogNodeId, productEntry.CatalogEntry[0].CatalogEntryId, &quot;&quot;, 
				new CatalogRelationResponseGroup(CatalogRelationResponseGroup.ResponseGroup.CatalogEntry));

			foreach (var variantDto in models)
			{
				if (!variantDto.Id.HasValue || variantDto.Id.GetValueOrDefault() &amp;lt;= 0)
					continue;

				// Variant - Category
				var variantNodeEntryRelationRow = categoryRelation.NodeEntryRelation.FirstOrDefault(i =&amp;gt; i.CatalogEntryId == variantDto.Id);

				if (variantNodeEntryRelationRow == null)
				{
					var variantRelation = catalogContextCurrent.GetCatalogRelationDto(variantDto.Id.Value);

					variantNodeEntryRelationRow = variantRelation.NodeEntryRelation.FirstOrDefault(i =&amp;gt; i.IsPrimary);
				}

				if (variantNodeEntryRelationRow == null)
				{
					variantNodeEntryRelationRow = categoryRelation.NodeEntryRelation.NewNodeEntryRelationRow();

					variantNodeEntryRelationRow.CatalogId = catalogId;
					variantNodeEntryRelationRow.CatalogEntryId = variantDto.Id.Value;
					variantNodeEntryRelationRow.CatalogNodeId = categoryEntry.CatalogNode[0].CatalogNodeId;
					variantNodeEntryRelationRow.IsPrimary = true;
					variantNodeEntryRelationRow.SortOrder = 0;

					if (variantNodeEntryRelationRow.RowState == DataRowState.Detached)
						categoryRelation.NodeEntryRelation.AddNodeEntryRelationRow(variantNodeEntryRelationRow);
				}
				else
				{
					variantNodeEntryRelationRow.CatalogId = catalogId;
					variantNodeEntryRelationRow.CatalogEntryId = variantDto.Id.Value;
					variantNodeEntryRelationRow.CatalogNodeId = categoryEntry.CatalogNode[0].CatalogNodeId;
					variantNodeEntryRelationRow.IsPrimary = true;
					variantNodeEntryRelationRow.SortOrder = 0;
				}

				// Product - Variant
				var variantCatalogEntryRelationRow = productRelation.CatalogEntryRelation.FirstOrDefault(i =&amp;gt; i.ChildEntryId == variantDto.Id);

				if (variantCatalogEntryRelationRow == null)
				{
					variantCatalogEntryRelationRow = productRelation.CatalogEntryRelation.NewCatalogEntryRelationRow();

					variantCatalogEntryRelationRow.ChildEntryId = variantDto.Id.Value;
					variantCatalogEntryRelationRow.ParentEntryId = productEntry.CatalogEntry[0].CatalogEntryId;
					variantCatalogEntryRelationRow.RelationTypeId = &quot;ProductVariation&quot;;
					variantCatalogEntryRelationRow.Quantity = 1;
					variantCatalogEntryRelationRow.GroupName = &quot;Default&quot;;
					variantCatalogEntryRelationRow.SortOrder = 0;

					if (variantCatalogEntryRelationRow.RowState == DataRowState.Detached)
						productRelation.CatalogEntryRelation.AddCatalogEntryRelationRow(variantCatalogEntryRelationRow);
				}
			}

			// Step 5: Save all relation changes
			try
			{
				if (productRelation.HasChanges())
					catalogContextCurrent.SaveCatalogRelationDto(productRelation);
			}
			catch (Exception ex)
			{
				var error = $&quot;VariantIntegrationService - Save Bulk - Error when saving product relations for product code = {product.Code}&quot;;
				Logger.Error(error, ex);

				result.AddError($&quot;{error} : {ex.Message}&quot;);
				return result;
			}

			try
			{
				if (categoryRelation.HasChanges())
					catalogContextCurrent.SaveCatalogRelationDto(categoryRelation);
			}
			catch (Exception ex)
			{
				var error = $&quot;VariantIntegrationService - Save Bulk - Error when saving category relations for product code = {product.Code}&quot;;
				Logger.Error(error, ex);

				result.AddError($&quot;{error} : {ex.Message}&quot;);
				return result;
			}
		}

		return result;
	}
		  
	#endregion

	#region Private Methods

	
	private void SaveCatalogEntryDtoByBatch(CatalogEntryDto dto)
	{
		if (dto.CatalogEntry.Count &amp;lt;= _batchSize)
		{
			CatalogContext.Current.SaveCatalogEntry(dto);
		}
		else
		{
			int index = 0;
			var newDto = new CatalogEntryDto();

			foreach (var catalogEntryRow in dto.CatalogEntry)
			{
				newDto.CatalogEntry.ImportRow(catalogEntryRow);

				foreach (var catalogItemSeoRow in catalogEntryRow.GetCatalogItemSeoRows())
					newDto.CatalogItemSeo.ImportRow(catalogItemSeoRow);

				++index;

				if (index % _batchSize == 0)
				{
					CatalogContext.Current.SaveCatalogEntry(newDto);
					newDto = new CatalogEntryDto();
				}
			}

			CatalogContext.Current.SaveCatalogEntry(newDto);
		}
	}

	private void SaveCatalogMetaObjectsByBatch(string language, IList&amp;lt;MetaObject&amp;gt; metaObjects)
	{
		var skip = 0;

		while (skip &amp;lt; metaObjects.Count)
		{
			var variantMetaObjects = metaObjects.Skip(skip).Take(_batchSize).ToList();

			// Save batch meta objects
			_catalogMetaObjectRepository.UpdateBatch(new Dictionary&amp;lt;string, IEnumerable&amp;lt;MetaObject&amp;gt;&amp;gt;()
			{
				{ language, variantMetaObjects }
			}, true);

			skip += variantMetaObjects.Count;
		}
	}

	private MetaDataContext CreateMetaDataContext(string language)
	{
		MetaDataContext metaDataContext = CatalogContext.MetaDataContext.Clone();
		metaDataContext.UseCurrentThreadCulture = false;
		metaDataContext.Language = language;
		return metaDataContext;
	}

	#endregion
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The extension class to set the properties on the variant &lt;code&gt;MetaObject&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static class ModelsExtensions
{
	public static void SetProperties(this MetaObject metaObject, BaseDTO model)
	{
		if (model == null || model.Properties == null)
			return;

		var values = metaObject.GetValues();

		foreach (var property in model.Properties)
		{
			if (!values.ContainsKey(property.Key))
				continue;

			metaObject.SetMetaField(property.Key, property.Value);
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Brief description of the above implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: &lt;strong&gt;Create all the variants&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Using the &lt;code&gt;CatalogEntryDto&lt;/code&gt; of the product we find the existing variant or we create a new catalog entry using &lt;code&gt;productEntry.CatalogEntry.NewCatalogEntryRow()&lt;/code&gt; method and a new variant entry using &lt;code&gt;productEntry.Variation.NewVariationRow()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;When creating a new variation row it is important to set the default values&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Step 2 : Save product variants changes&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;If the product entry has changes, then save the changes by batches using &lt;code&gt;SaveCatalogEntryDtoByBatch(productEntry)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Once saved by batches, initialize the &lt;code&gt;variantDto.Id&lt;/code&gt; as it will be needed later on to save the variant properties&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Step 3: Save variant properties (meta objects)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Get the &lt;code&gt;MetaClass&lt;/code&gt; of the &lt;code&gt;WebVariant&lt;/code&gt; type using the &lt;code&gt;MetaClass.Load()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Initialize a meta data context: &lt;code&gt;CreateMetaDataContext(language)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Using the &lt;code&gt;CatalogMetaObjectRepository&lt;/code&gt; repository, for each variant load or create the &lt;code&gt;MetaObject&lt;/code&gt; and save the properties.&lt;/li&gt;
&lt;li&gt;Use an extension methods to save all properties from the dictionary to the meta object: &lt;code&gt;metaObject.SetProperties(variantDto);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Save catalog meta objects by batch&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Step 4: Create all the relations (variant - category AND product - variant)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;A variant belongs to a category and has a &lt;code&gt;NodeEntryRelation&lt;/code&gt; with a category&lt;/li&gt;
&lt;li&gt;A variant is related to a product and has a &lt;code&gt;CatalogEntryRelation&lt;/code&gt; with the product&lt;/li&gt;
&lt;li&gt;For each variant, build the variant-category relation and product-variant relation using the &lt;code&gt;GetCatalogRelationDto()&lt;/code&gt;, &lt;code&gt;categoryRelation.NodeEntryRelation.NewNodeEntryRelationRow()&lt;/code&gt; and the &lt;code&gt;productRelation.CatalogEntryRelation.NewCatalogEntryRelationRow()&lt;/code&gt; methods&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Step 5: Save all relation changes&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;If there are product and category relations, use the &lt;code&gt;SaveCatalogRelationDto()&lt;/code&gt; to save the changes&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Return the result model&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;During the entire process of importing the variants, if there are any errors or exceptions, these are logged and captured in the &lt;code&gt;ResultModelDTO.cs&lt;/code&gt; to be returned to the upper calling layers.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;4. The scheduled job caller method - D365SyncProductsScheduledJob.cs&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The variant integration service is called during the &lt;code&gt;InternalExecute()&lt;/code&gt; method of the scheduled job to sync all products from D365 into Episerver.&lt;/p&gt;
&lt;p&gt;A snippet of the pseudo code that is triggering the save bulk variants implementation:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;private async Task&amp;lt;D365SyncResultModel&amp;gt; SyncProductsInternal(IList&amp;lt;ProductModel&amp;gt; products)
{
	&amp;hellip;
	foreach (var product in products)
    {
		// Get product variants		
		var productWithVariants = await syncService.GetProductById(channelId, catalogId, catalogName, product.RecordId);

        product.Variants = productWithVariants.Variants.ToList();

		// Create product dto and variant dtos
	    var productDto = ToProductDto(product, language, webCategory);

	    var resultModelSaveProductDto = SaveProductDto(productDto);
		&amp;hellip;
        
		// Bulk save variants
		if (productDto.Variants.Count &amp;gt; 0)
		{
			var resultModelSaveVariantDtos = SaveVariantDtos(productDto.Variants);

			resultModel.AddExceptions(resultModelSaveVariantDtos.Exceptions);

			// Fallback to individual save variant (1 by 1)
			if (!resultModelSaveVariantDtos.IsSuccess)
			{
				ChangeStatus($&quot;Bulk Sync {productDto.Variants.Count} Variants for product code {resultModelSaveProductDto.Model.Code} - Failed. Sync variants 1 by 1 will take a while...&quot;, true);

				Parallel.ForEach(productDto.Variants, variantDto =&amp;gt;
				{
					SaveVariantDto(variantDto);
				});
			}
		}
	}
	&amp;hellip;
}

private ResultModel SaveVariantDtos(IList&amp;lt;VariantDTO&amp;gt; dtos)
{
	var resultModel = new ResultModel&amp;lt;ResultModelDTO&amp;gt;();

	try
	{
		resultModel.Model = _variantIntegrationService.SaveBulk(dtos);

		if (resultModel.Model.IsFail)
			LogError(new ApplicationException($&quot;Product {dtos.First().Product.Code} variants not saved successfully. See logs for error details.&quot;), resultModel);
	  
		resultModel.AddExceptions(resultModel.Model.Exceptions);
	}
	catch (Exception exc)
	{
		LogError(new ApplicationException($&quot;Product {dtos.First().Product.Code} variants not saved successfully. An exception error has been logged!&quot;, exc), resultModel);
	}

	return resultModel;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We build a list of products and variants DTOs that we save in bulk and if it fails we fallback to the Catalog content APIs to save the variants one by one.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;ToProductDto()&lt;/code&gt;&amp;nbsp;converts the &lt;code&gt;ProductModel&lt;/code&gt; we have from our &lt;code&gt;SyncService.cs&lt;/code&gt; into a DTO model used to import the Product content into Episerver:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;private ProductDTO ToProductDto(ProductModel product, string language, WebCategory webCategory, bool skipProperties = false)
{
	var productDto = new ProductDTO()
	{
		CatalogName = product.CatalogName,
		Category = webCategory,
		Code = product.GetCode(),
		Language = language,
		SourceObject = !skipProperties
						? product.SourceObject
						: null
	};

	var propertyMapper = new D365PropertyMapper(D365Settings.RetailServerSelectDefaultValue, language);

	productDto.Name = GetProductName(product, language);

	productDto.DisplayName = propertyMapper.GetPropertyValue(product?.ProductProperties, &quot;Description&quot;) ?? productDto.Name;
	productDto.Description = propertyMapper.GetPropertyValue(product?.ProductProperties, &quot;ProductDescription&quot;);

	productDto.Properties = propertyMapper.MapProperties(product, language);
	
	productDto.Variants = product.Variants?.Select(variant =&amp;gt;
	{
		var variantDto = !skipProperties
						? ToVariantDto(product, variant, language)
						: ToVariantDtoImages(product, variant, language);

		variantDto.Category = productDto.Category;
		variantDto.ProductName = productDto.Name;

		return variantDto;

	}).ToList() ?? new List&amp;lt;VariantDTO&amp;gt;();

	return productDto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ToVariantDto()&lt;/code&gt; converts the &lt;code&gt;ProductVariantModel&lt;/code&gt; into a DTO model used to import the Variation content into Episerver:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;private VariantDTO ToVariantDto(ProductModel product, ProductVariantModel variant, string language, bool skipProperties = false)
{
	var variantName = GetVariantName(variant, language);

	var variantDto = new VariantDTO()
	{
		CatalogName = product.CatalogName,
		Code = variant.GetCode(),
		Name = variantName,
		DisplayName = variantName,
		Language = language,
		SourceObject = variant.SourceObject
	};

	if (skipProperties)
		return variantDto;

	var propertyMapper = new D365PropertyMapper(D365Settings.RetailServerSelectDefaultValue, language);
	
	variantDto.Properties = propertyMapper.MapProperties(variant, language);

	return variantDto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Due to the number of variants under a product, we also setup the following property in the configuration file:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;add key=&quot;SimplifiedCatalogListingThreshold&quot; value=&quot;100000&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;Conclusion&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;As mentioned in Episerver Commerce documentation, the recommended approach of working with the catalog is using the Catalog content APIs. We are using these APIs for importing the catalog, categories, products, relations and more.&lt;/p&gt;
&lt;p&gt;However, in specific scenarios, like ours, the Episerver Commerce low-level APIs are useful for working with the Catalog.&lt;/p&gt;
&lt;p&gt;It allowed us to reduce significantly the synchronization time and to &lt;strong&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;sync a full catalog with 85 products and ~ 100 000 variants in less than 1.5h&lt;/span&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I hope it will help you too when importing large catalogs like in our use case!&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The Episerver Commerce low-level APIs are described here: &lt;a href=&quot;/link/f3d3cfcf60be45b4be2bbcefdea47816.aspx&quot;&gt;https://world.episerver.com/documentation/developer-guides/commerce/catalogs/low-level-apis/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;I hope you enjoyed the reading and the proposed solution. Looking forward to your feedback! Thank you!&lt;/p&gt;</id><updated>2020-08-16T22:46:17.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Configurable Episerver Find Facets</title><link href="https://world.optimizely.com/blogs/calin-lupas/dates/2019/12/configurable-episerver-find-facets-by-editors/" /><id>&lt;p&gt;The proposed solution aims to offer an easy way for content editors to dynamically change the Find facet filters and proposes full flexibly on the filters used to narrow down the search results and guide the user experience in finding the right products.&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;Introduction&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Searching and finding the product you want to buy is a key feature for an E-commerce solution. Having the flexibility to narrow down search results based on different criteria improves the user experience and facilitates the finding of the product that the customer is looking for.&lt;/p&gt;
&lt;p&gt;Episerver Find facets allow us to group search results based on specific terms, price ranges and other criteria that we can establish based on the project requirements.&lt;/p&gt;
&lt;p&gt;During the design phase of the project we establish the product attributes and the facets we want to filter the search results with the client. We end-up with a list of facets displayed as filter criteria on the search results page or on the category landing page.&lt;/p&gt;
&lt;p&gt;We often hard-code the facets configuration (what facets, the display order, the display type, the numeric ranges, etc.) based on the initial design we agreed with the client.&lt;/p&gt;
&lt;p&gt;What if, after we go live, the client wants to add more facets, change the facets order, add new numeric ranges or even remove some existing facets? We will need to change the code, test and redeploy the hotfix into Production.&lt;/p&gt;
&lt;p&gt;Would it be nice to allow CMS editors or administrators to configure the Find facets dynamically from Episerver CMS Start page?&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;Solution&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Implement a solution that allows dynamic configuration of Find facets by CMS content editors.&lt;/p&gt;
&lt;p&gt;This blog post will describe in detail the solution to dynamically configure the Find facets.&lt;/p&gt;
&lt;p&gt;The code is part of Episerver Foundation and can be found here: &lt;a href=&quot;https://github.com/episerver/Foundation&quot;&gt;https://github.com/episerver/Foundation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/1a18ceb2a8524adc9bb4606899d4bf36.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;How it works?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Episerver Foundation defines by default in code the following facets: Price, Brand, Size and Color and registers them as facets to be displayed on the search results page and be used by the Find engine:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/6e6b7c742dbc414f8249170979128a38.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In Episerver &lt;em&gt;CMS &amp;gt; Home page&lt;/em&gt;, under &lt;em&gt;Search settings&lt;/em&gt; tab, we&amp;rsquo;ve added a new field called &amp;ldquo;&lt;em&gt;Search Filters Configuration&lt;/em&gt;&amp;rdquo; that allows content editors to dynamically define and configure the facets displayed on the search results page:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/24b795d1336d4e909e843e6cbe282c1f.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Content editors can add, remove and move up/down the filters to reorder them, change the numeric ranges, display mode and direction, exclude specific values or display only specific values.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/9f3b196bc5a14c64a34882d76b3c3005.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As a result, after we save the search filters configuration and publish the Home page, the new facets will be displayed on the search results page:&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/4e6daf24c4a24799b6ada48753501b0f.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;Implementation&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s go into the detail and explain the solution from a code implementation perspective.&lt;/p&gt;
&lt;p&gt;The solution consists of &lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;3 main steps&lt;/span&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define the &lt;strong&gt;facet configuration model&lt;/strong&gt; and include it in the Home page as a new property list.&lt;/li&gt;
&lt;li&gt;Implement the &lt;strong&gt;facet configuration factory&lt;/strong&gt; that will transform the configuration model into a facet definition and provide the list of facet filters.&lt;/li&gt;
&lt;li&gt;Configure the&amp;nbsp;&lt;strong&gt;facet configuration initialization&lt;/strong&gt; and registration of all the facet definitions.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;1. Facet Configuration Model&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;We first need to define an interface &lt;code&gt;IFacetConfiguration.cs&amp;nbsp;&lt;/code&gt;that will contain the definition of the new search configuration field:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public interface IFacetConfiguration
{
    IList&amp;lt;FacetFilterConfigurationItem&amp;gt; SearchFiltersConfiguration { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We implement this interface in the &lt;code&gt;DemoHomePage.cs&lt;/code&gt; page and define the new field as follows:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class DemoHomePage : CommerceHomePage, IFacetConfiguration
{
  &amp;hellip;

  #region Search Settings
  &amp;hellip;
  [Display(
          Name = &quot;Search Filters Configuration&quot;,
          Description = &quot;Manage filters to be displayed on Search&quot;,
          GroupName = CommerceTabNames.SearchSettings,
          Order = 300)]
        [EditorDescriptor(EditorDescriptorType = typeof(IgnoreCollectionEditorDescriptor&amp;lt;FacetFilterConfigurationItem&amp;gt;))]
        public virtual IList&amp;lt;FacetFilterConfigurationItem&amp;gt; SearchFiltersConfiguration { get; set; }
  #endregion
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Since we use a list of facet configuration items, we have implemented a custom &lt;code&gt;PropertyListBase.cs&lt;/code&gt; and defined our own configuration property:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[PropertyDefinitionTypePlugIn]
public class FacetFilterConfigurationProperty : PropertyListBase&amp;lt;FacetFilterConfigurationItem&amp;gt;
{
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The new model &lt;code&gt;FacetFilterConfigurationItem.cs&amp;nbsp;&lt;/code&gt;contains all the properties needed to allow the facet definition and the customization of the search filters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Attribute as Filter&lt;/strong&gt; (FieldName)
&lt;ul&gt;
&lt;li&gt;The name of the field indexed by Find; Attribute to be used as a filter&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Display Name&lt;/strong&gt; (DisplayName)
&lt;ul&gt;
&lt;li&gt;Display name for filter group&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filter Type&lt;/strong&gt; (FieldType)
&lt;ul&gt;
&lt;li&gt;Data type of the attribute&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Display Mode&lt;/strong&gt; (DisplayMode)
&lt;ul&gt;
&lt;li&gt;How the values of the filter are displayed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Display Direction&lt;/strong&gt; (DisplayDirection)
&lt;ul&gt;
&lt;li&gt;The direction of the facet option values: vertical or horizontal&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Numeric Ranges&lt;/strong&gt; &lt;strong&gt;(From-To)&lt;/strong&gt; (NumericRanges)
&lt;ul&gt;
&lt;li&gt;Set of ranges based on field type in format: from-to, from- and -to&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exclude Flag Attributes&lt;/strong&gt; &lt;strong&gt;or Specific Values&lt;/strong&gt; (ExcludeFlagFields)
&lt;ul&gt;
&lt;li&gt;Exclude specific attributes from Flags or specific values of an attribute&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Display Specific Values&lt;/strong&gt; (DisplaySpecificValues)
&lt;ul&gt;
&lt;li&gt;Display only specific values of the facet&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public class FacetFilterConfigurationItem
    {
        public FacetFilterConfigurationItem()
        {
            FieldType = FacetFieldType.String.ToString();
            DisplayMode = FacetDisplayMode.Checkbox.ToString();
            DisplayDirection = FacetDisplayDirection.Vertical.ToString();
        } 

        [Display(
           Name = &quot;Attribute as Filter (required)&quot;,
           Description = &quot;Attribute to be used as a filter&quot;,
           Order = 1)]
        [Required]
        public virtual string FieldName { get; set; }
 
        [Display(
           Name = &quot;Display Name&quot;,
           Description = &quot;Display name for filter in English&quot;)]
        public virtual string DisplayNameEN { get; set; } 

        [Display(
          Name = &quot;Display Name (FR)&quot;,
          Description = &quot;Display name for filter in French&quot;)]
        [Ignore]
        [ScaffoldColumn(false)]
        public virtual string DisplayNameFR { get; set; }
 
        [Display(
            Name = &quot;Filter Type (required)&quot;,
            Description = &quot;Data type of attribute&quot;)]
        [SelectOneEnum(typeof(FacetFieldType))]
        [DefaultValue(FacetFieldType.String)]
        [Required]
        public virtual string FieldType { get; set; }
  
        [Display(
            Name = &quot;Display Mode (required)&quot;,
            Description = &quot;How the values of the filter are displayed&quot;)]
        [SelectOneEnum(typeof(FacetDisplayMode))]
        [DefaultValue(FacetDisplayMode.Button)]
        [Required]
        public virtual string DisplayMode { get; set; }
        
        [Display(
              Name = &quot;Display direction (optional)&quot;,
              Description = &quot;Only applies to color swatch and size swatch.&quot;)]
        [SelectOneEnum(typeof(FacetDisplayDirection))]
        [DefaultValue(FacetDisplayDirection.Vertical)]
        public virtual string DisplayDirection { get; set; }

        [Display(
            Name = &quot;Numeric Ranges (From-To)&quot;,
            Description = &quot;Set ranges based on field type in format: from-to, from- and -to.&quot;)]
        [ItemRegularExpression(&quot;[0-9]*\\.?[0-9]*-[0-9]*\\.?[0-9]*&quot;)]
        public virtual IList&amp;lt;string&amp;gt; NumericRanges { get; set; }

        [Display(
            Name = &quot;Exclude Flag Attributes or Specific Values&quot;,
            Description = &quot;Used to exclude specific attributes from Flags or specific values of an attribute&quot;)]
        public virtual IList&amp;lt;string&amp;gt; ExcludeFlagFields { get; set; }
 
        [Display(
            Name = &quot;Display Specific Values&quot;,
            Description = &quot;Used to display specific values of an Attribute as Filter: e.g. Brand. Must be exact match to value of attribute.&quot;)]
        public virtual IList&amp;lt;string&amp;gt; DisplaySpecificValues { get; set; }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;To ease the definition of the facet configuration item, we&amp;rsquo;ve create the following enumerations within&amp;nbsp;&lt;code&gt;FacetEnums.cs&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FacetFieldType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FacetDisplayMode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FacetDisplayDirection&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public enum FacetFieldType
    {
        [EnumSelectionDescription(Text = &quot;String&quot;, Value = &quot;String&quot;)]
        String = 1,
        [EnumSelectionDescription(Text = &quot;List of String&quot;, Value = &quot;ListOfString&quot;)]
        ListOfString,
        [EnumSelectionDescription(Text = &quot;Integer&quot;, Value = &quot;Integer&quot;)]
        Integer,
        [EnumSelectionDescription(Text = &quot;2 Decimal Places&quot;, Value = &quot;Double&quot;)]
        Double,
        [EnumSelectionDescription(Text = &quot;Boolean&quot;, Value = &quot;Boolean&quot;)]
        Boolean,
        [EnumSelectionDescription(Text = &quot;Enhanced Boolean&quot;, Value = &quot;NullableBoolean&quot;)]
        NullableBoolean
    } 

    public enum FacetDisplayMode
    {
        [EnumSelectionDescription(Text = &quot;Checkbox&quot;, Value = &quot;Checkbox&quot;)]
        Checkbox = 1,
        [EnumSelectionDescription(Text = &quot;Button&quot;, Value = &quot;Button&quot;)]
        Button,
        [EnumSelectionDescription(Text = &quot;Color Swatch&quot;, Value = &quot;ColorSwatch&quot;)]
        ColorSwatch,
        [EnumSelectionDescription(Text = &quot;Size Swatch&quot;, Value = &quot;SizeSwatch&quot;)]
        SizeSwatch,
        [EnumSelectionDescription(Text = &quot;Numeric Range&quot;, Value = &quot;Range&quot;)]
        Range,
        [EnumSelectionDescription(Text = &quot;Rating&quot;, Value = &quot;Rating&quot;)]
        Rating,
        [EnumSelectionDescription(Text = &quot;Slider&quot;, Value = &quot;Slider&quot;)]
        Slider,
        [EnumSelectionDescription(Text = &quot;Price Range&quot;, Value = &quot;PriceRange&quot;)]
        PriceRange,
    } 

    public enum FacetDisplayDirection
    {
        [EnumSelectionDescription(Text = &quot;Vertical&quot;, Value = &quot;Vertical&quot;)]
        Vertical = 1,
        [EnumSelectionDescription(Text = &quot;Horizontal&quot;, Value = &quot;Horizontal&quot;)]
        Horizontal
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;To allow the selection of one option from these listings we created &lt;code&gt;SelectOneEnumAttribute.cs&lt;/code&gt; and apply it to the property as follows:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;       [Display(
            Name = &quot;Filter Type (required)&quot;,
            Description = &quot;Data type of attribute&quot;)]
        [SelectOneEnum(typeof(FacetFieldType))]
        [DefaultValue(FacetFieldType.String)]
        [Required]
        public virtual string FieldType { get; set; }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;SelectOneEnumAttribute.cs&lt;/code&gt; attribute will allow editors to select a value of the enumeration:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public class SelectOneEnumAttribute : SelectOneAttribute, IMetadataAware
    {
        public SelectOneEnumAttribute(Type enumType)
        {
            EnumType = enumType;
        }
 
        public Type EnumType { get; set; }

        public new void OnMetadataCreated(ModelMetadata metadata)
        {
            var enumType = metadata.ModelType; 

            SelectionFactoryType = typeof(EnumSelectionFactory&amp;lt;&amp;gt;).MakeGenericType(EnumType); 

            base.OnMetadataCreated(metadata);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Until now we defined the new search configuration property under the home page and added all the necessary code to allow content editors to define and configure the facets:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/6985163c9a704a378022f12252cd0ac8.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The next steps will be to define the facet configuration factory.&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;2. Facet Configuration Factory&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The facet configuration factory will transform the configuration model into a facet definition and provide the list of facet configuration items.&lt;/p&gt;
&lt;p&gt;The interface &lt;code&gt;IFacetConfigFactory.cs&lt;/code&gt; defines 3 methods:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&lt;em&gt;GetDefaultFacetDefinitions&lt;/em&gt;()&lt;/code&gt; &amp;ndash; returns the default facet definitions: Price, Brand, Size and Color.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&lt;em&gt;GetFacetFilterConfigurationItems&lt;/em&gt;()&lt;/code&gt; &amp;ndash; returns the list of facet configuration models that are configured on the start page.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&lt;em&gt;GetFacetDefinition&lt;/em&gt;()&lt;/code&gt; &amp;ndash; converts a facet configuration model into a facet definition that is used by the Find search service implementation.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public interface IFacetConfigFactory
    {
        List&amp;lt;FacetDefinition&amp;gt; GetDefaultFacetDefinitions();

        List&amp;lt;FacetFilterConfigurationItem&amp;gt; GetFacetFilterConfigurationItems();

        FacetDefinition GetFacetDefinition(FacetFilterConfigurationItem facetConfiguration);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;FacetConfigFactory.cs&lt;/code&gt; is default implementation of this interface and implements the methods as follow:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public class FacetConfigFactory : IFacetConfigFactory
    {
        private readonly IContentLoader _contentLoader;

        public FacetConfigFactory(IContentLoader contentLoader)
        {
            _contentLoader = contentLoader;
        }

        public virtual List&amp;lt;FacetDefinition&amp;gt; GetDefaultFacetDefinitions()
        {
            return new List&amp;lt;FacetDefinition&amp;gt;();
        }

        public virtual FacetDefinition GetFacetDefinition(FacetFilterConfigurationItem facetConfiguration)
        {
            switch (Enum.Parse(typeof(FacetFieldType), facetConfiguration.FieldType))
            {
                case FacetFieldType.String:
                    return new FacetStringDefinition
                    {
                        FieldName = facetConfiguration.FieldName,
                        DisplayName = facetConfiguration.GetDisplayName()
                    };

                case FacetFieldType.ListOfString:
                    return new FacetStringListDefinition
                    {
                        FieldName = facetConfiguration.FieldName,
                        DisplayName = facetConfiguration.GetDisplayName()
                    };

                case FacetFieldType.Boolean:
                case FacetFieldType.NullableBoolean:
                    return new FacetStringListDefinition
                    {
                        FieldName = facetConfiguration.FieldName,
                        DisplayName = facetConfiguration.GetDisplayName(),
                    };
            }

            return new FacetStringDefinition
            {
                FieldName = facetConfiguration.FieldName,
                DisplayName = facetConfiguration.GetDisplayName(),
            };
        }

        public List&amp;lt;FacetFilterConfigurationItem&amp;gt; GetFacetFilterConfigurationItems()
        {
            if (ContentReference.IsNullOrEmpty(ContentReference.StartPage))
            {
                return new List&amp;lt;FacetFilterConfigurationItem&amp;gt;();
            }

            var startPage = _contentLoader.Get&amp;lt;IContent&amp;gt;(ContentReference.StartPage);

            var facetsConfiguration = startPage as IFacetConfiguration;
            if (facetsConfiguration?.SearchFiltersConfiguration != null)
            {
                return facetsConfiguration
                    .SearchFiltersConfiguration
                    .ToList();
            }

            return new List&amp;lt;FacetFilterConfigurationItem&amp;gt;();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Based on the facet configuration model field type we create instances of &lt;code&gt;&lt;em&gt;FacetStringDefinition&lt;/em&gt; &lt;/code&gt;or &lt;code&gt;&lt;em&gt;FacetStringListDefinition&lt;/em&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The commerce facet configuration factory &lt;code&gt;CommerceFacetConfigFactory.cs&lt;/code&gt; inherits from the default implementation and override the &lt;code&gt;&lt;em&gt;GetFacetDefinition()&lt;/em&gt; &lt;/code&gt;and &lt;code&gt;&lt;em&gt;GetDefaultFacetDefinitions()&lt;/em&gt; &lt;/code&gt;methods.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public class CommerceFacetConfigFactory : FacetConfigFactory
    {
        private readonly ICurrentMarket _currentMarket;

        public CommerceFacetConfigFactory(ICurrentMarket currentMarket,
            IContentLoader contentLoader) : base(contentLoader)
        {
            _currentMarket = currentMarket;
        }

        public override FacetDefinition GetFacetDefinition(FacetFilterConfigurationItem facetConfiguration)
        {
            switch (Enum.Parse(typeof(FacetFieldType), facetConfiguration.FieldType))
            {
                case FacetFieldType.Integer:
                    return new FacetNumericRangeDefinition(_currentMarket)
                    {
                        FieldName = facetConfiguration.FieldName,
                        DisplayName = facetConfiguration.GetDisplayName(),
                        BackingType = typeof(int)
                    };

                case FacetFieldType.Double:
                    if (facetConfiguration.DisplayMode == FacetDisplayMode.Range.ToString()
                        || facetConfiguration.DisplayMode == FacetDisplayMode.PriceRange.ToString())
                    {
                        var rangeDefinition = new FacetNumericRangeDefinition(_currentMarket)
                        {
                            FieldName = facetConfiguration.FieldName,
                            DisplayName = facetConfiguration.GetDisplayName(),
                            BackingType = typeof(double)
                        };

                        rangeDefinition.Range = facetConfiguration.GetSelectableNumericRanges();

                        return rangeDefinition;
                    }
                    else if (facetConfiguration.DisplayMode == FacetDisplayMode.Rating.ToString())
                    {
                        var rangeDefinition = new FacetAverageRatingDefinition(_currentMarket)
                        {
                            FieldName = facetConfiguration.FieldName,
                            DisplayName = facetConfiguration.GetDisplayName(),
                            BackingType = typeof(double)
                        };

                        rangeDefinition.Range = facetConfiguration.GetSelectableNumericRanges();

                        return rangeDefinition;
                    }
                    break;
            }

            return base.GetFacetDefinition(facetConfiguration);
        }

        public override List&amp;lt;FacetDefinition&amp;gt; GetDefaultFacetDefinitions()
        {
            var brand = new FacetStringDefinition
            {
                FieldName = &quot;Brand&quot;,
                DisplayName = &quot;Brand&quot;
            };

            var color = new FacetStringListDefinition
            {
                DisplayName = &quot;Color&quot;,
                FieldName = &quot;AvailableColors&quot;
            };

            var size = new FacetStringListDefinition
            {
                DisplayName = &quot;Size&quot;,
                FieldName = &quot;AvailableSizes&quot;
            };

            var priceRanges = new FacetNumericRangeDefinition(_currentMarket)
            {
                DisplayName = &quot;Price&quot;,
                FieldName = &quot;DefaultPrice&quot;,
                BackingType = typeof(double)

            };
            priceRanges.Range.Add(new SelectableNumericRange() { To = 50 });
            priceRanges.Range.Add(new SelectableNumericRange() { From = 50, To = 100 });
            priceRanges.Range.Add(new SelectableNumericRange() { From = 100, To = 500 });
            priceRanges.Range.Add(new SelectableNumericRange() { From = 500, To = 1000 });
            priceRanges.Range.Add(new SelectableNumericRange() { From = 1000 });

            return new List&amp;lt;FacetDefinition&amp;gt;() { priceRanges, brand, size, color };
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Based on the facet configuration model field type and display mode we create instances of &lt;code&gt;FacetNumericRangeDefinition.cs&lt;/code&gt; or &lt;code&gt;FacetAverageRatingDefinition.cs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We also define the 4 default facet definitions: Price, Brand, Size and Color.&lt;/p&gt;
&lt;p&gt;With the facet configuration factory and facet configuration model ready, the only step remaining is to initialize the facet configuration during the initialization module.&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;3. Facet Configuration Initialization&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;First, we need to register the 2 new facet configuration factories in the initialization module &lt;code&gt;Initialize.cs&lt;/code&gt;&amp;nbsp;of &lt;em&gt;Find.Cms&lt;/em&gt; and &lt;em&gt;Find.Commerce&lt;/em&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace Foundation.Find.Cms
{
    [ModuleDependency(typeof(Foundation.Cms.Initialize))]
    public class Initialize : IConfigurableModule
    {
        void IConfigurableModule.ConfigureContainer(ServiceConfigurationContext context)
        {
            &amp;hellip;
            services.AddSingleton&amp;lt;IFacetConfigFactory, FacetConfigFactory&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace Foundation.Find.Commerce
{
    [ModuleDependency(typeof(Cms.Initialize), typeof(FindCommerceInitializationModule))]
    public class Initialize : IConfigurableModule
    {
        void IConfigurableModule.ConfigureContainer(ServiceConfigurationContext context)
        {
           &amp;hellip;
           services.AddSingleton&amp;lt;IFacetConfigFactory, CommerceFacetConfigFactory&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have the code in place to dynamically configure the Find facets. We need now to write the code to glue everything together during the initialize module and every time the start page is published.&lt;/p&gt;
&lt;p&gt;Episerver Foundation uses the &lt;code&gt;&lt;em&gt;IFacetRegistry.cs&lt;/em&gt;&lt;/code&gt; interface to register the Find facets that will be used by the Find service and that will be displayed on the search results page.&lt;/p&gt;
&lt;p&gt;To facilitate the integration into the initialization engine we implemented an &lt;code&gt;&lt;em&gt;InitializationEngine&lt;/em&gt; &lt;/code&gt;extension as follows:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public static class InitializationEngineExtensions
    {
        private static Lazy&amp;lt;IContentEvents&amp;gt; _contentEvents = new Lazy&amp;lt;IContentEvents&amp;gt;(() =&amp;gt; ServiceLocator.Current.GetInstance&amp;lt;IContentEvents&amp;gt;());
        private static Lazy&amp;lt;IFacetRegistry&amp;gt; _facetRegistry = new Lazy&amp;lt;IFacetRegistry&amp;gt;(() =&amp;gt; ServiceLocator.Current.GetInstance&amp;lt;IFacetRegistry&amp;gt;());
        private static Lazy&amp;lt;IFacetConfigFactory&amp;gt; _facetConfigFactory = new Lazy&amp;lt;IFacetConfigFactory&amp;gt;(() =&amp;gt; ServiceLocator.Current.GetInstance&amp;lt;IFacetConfigFactory&amp;gt;());

        public static void InitializeFoundationFindCms(this InitializationEngine context)
        {
            InitializeFacets(_facetConfigFactory.Value.GetFacetFilterConfigurationItems());

            _contentEvents.Value.PublishedContent += OnPublishedContent;
        }

        static void OnPublishedContent(object sender, ContentEventArgs contentEventArgs)
        {
            if (contentEventArgs.Content is IFacetConfiguration facetConfiguration)
            {
                InitializeFacets(facetConfiguration.SearchFiltersConfiguration);
            }
        }

        private static void InitializeFacets(IList&amp;lt;FacetFilterConfigurationItem&amp;gt; configItems)
        {
            _facetRegistry.Value.Clear();

            if (configItems != null &amp;amp;&amp;amp; configItems.Any())
            {
                configItems
                    .ToList()
                    .ForEach(x =&amp;gt; _facetRegistry.Value.AddFacetDefinitions(_facetConfigFactory.Value.GetFacetDefinition(x)));
            }
            else
            {
                _facetConfigFactory.Value.GetDefaultFacetDefinitions()
                    .ForEach(x =&amp;gt; _facetRegistry.Value.AddFacetDefinitions(x));
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using the facet configuration factory, we register the facet definitions configured by the content editors in start page or we fallback to the default one.&lt;/p&gt;
&lt;p&gt;When the start page is published we listen to the published content even and reset the facet registration with the new facet definitions and configurations.&lt;/p&gt;
&lt;p&gt;The last thing to do is to call the initialization engine extension we&amp;rsquo;ve created inside the initialize method of the &lt;code&gt;InitializeSite.cs&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace Foundation.Infrastructure
{
    public class InitializeSite : IConfigurableModule
    {
        &amp;hellip;

        public void Initialize(InitializationEngine context)
        {
         &amp;hellip;
            context.InitializeFoundationCms();
            context.InitializeFoundationCommerce();
            context.InitializeFoundationFindCms();
            context.InitializeFoundationDemo();&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;span style=&quot;text-decoration:&amp;#32;underline;&quot;&gt;Conclusion&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Enabling the configuration of the search facets and allowing live changes of what the end-user will see and filter on, brings an added value to any e-commerce solution and allows enrichment of the search engine.&lt;/p&gt;
&lt;p&gt;The enrichments and next steps of the solution include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Display modes&lt;/em&gt;: color swatches, checkboxes, sliders, etc. Use the facet display mode in the view to call a partial view that displays the facet options in different ways;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Display direction&lt;/em&gt;: vertically or horizontally. For some facets we might want to display the facet options horizontally, one beside the other, like for Size;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Exclude flags:&lt;/em&gt; for Boolean facets we might want to group all of them under one facet group, like Status and have facet options like New, On Sale, Featured, Available on Store, etc.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Exclude values:&lt;/em&gt; Exclude specific values that we do not want to be displayed as facet options; and&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Display specific values&lt;/em&gt;: Only display specific values in case we have too many facet options we might want to display on the top brands.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The facet configuration model properties can be transferred to the facet definition &lt;code&gt;FacetDefinition.cs&lt;/code&gt; and implement the additional logic inside &lt;code&gt;PopulateFacet()&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;The facet configuration can be then transferred to the &lt;code&gt;FacetGroupOption.cs&lt;/code&gt; and used in the view to customize the display of a facet by rendering a partial view for each display mode.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I hope you enjoyed the reading and the proposed solution. Looking forward to your feedback!&amp;nbsp;Thank you!&lt;/p&gt;
&lt;p&gt;The full source code is available on Episerver Foundation GitHub repository: &lt;a href=&quot;https://github.com/episerver/Foundation&quot;&gt;https://github.com/episerver/Foundation&lt;/a&gt;&lt;/p&gt;</id><updated>2019-12-15T13:48:20.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>