SXC9 Sample Code to Create or Update Commerce Catalog Entities

SergeyYatsenko
Sitecore Technology MVP & Sr. Director
  • Twitter
  • LinkedIn

SXC9 - Sample Code to Create or Update Commerce Catalog Entities


This post provides some code samples that can be adapted and used to create and update Commerce Catalog entities from Commerce Engine code. Sitecore Experience Commerce documentation and numerous blog posts on this subject are great, but I d like to share few code samples, adapted from current real-world projects, something that can hopefully help those building their first Commerce projects.

Sitecore Experience Commerce 9 (SXC9) allows to manipulate following types of Catalog entities in its catalog system out of the box (OOB):

  • Catalogs: those are the top-level containers for available on website. Website don t have to be limited to just one Catalog, it s just usually is the case.
  • Categories: container to hold products, grouped by some criteria. Categories need to belong to a Catalog and can be nested within other categories, allowing to create hierarchies.
  • Sellable Items: Those are the actual products, something that can be sold (e.g. a physical item, a service, a digital download). Sellable Items would usually have a Category as their parent
  • Sellable Item Variants: those product variations, such as color, size, caliber, etc. Variants cannot exist by themselves, they always belong with a given product and always stored as part of the product.

Commerce Catalog is exposed to commerce via Commerce Connect data provider, making it appear as part of Sitecore Content Tree, so in Sitecore Content Editor it would look similar to this:

Commerce Catalog entities are not stored in Sitecore and don t always behave same as other content items. Few major things to keep in mind when working with Commerce Catalog items:

  • Categories and Sellable Items can appear in multiple places in content tree, it s perfectly fine to have the same product under
  • Commerce Connect data provider is read only, so commerce items cannot be updated via Sitecore APIs or content editor
  • When it comes to publishing and item versioning, Commerce supports this starting with Commerce 9.0.2, but under the hood things are very different. Sitecore Master and Web databases are connected to the same Commerce Provider data source, when Commerce items are published, items are not copied from master to web database, they stay where they are, only their workflow state is changed.
  • For the purpose of this post, I ll assume you already have Commerce solution set up and configured for your environment. If not, thenSitecore.Commerce.Engine.SDK.2.2.72.zip, which is a part of Sitecore Experience Commerce install package, has sample Commerce solution, which can be a good starting point.

To shorten sample code in this post, I removed all checks, logging and error handling it s a must have for production-ready code, but the subject of this post, so we ll skip on that that. I also didn t spend much time testing and validating this code just extracted from some of my current projects, simplified, obfuscated and added some comments for clarity. Please don t hold me responsible for anything ? this is just a boilerplate code for illustration purposes, it is meant to be updated, validated and tested by you to fit your needs.

Async, Await pattern

All Commerce commands are written to be called asynchronously, so they d need to be invoked like this:

var entity = await _createCatalogCommand.Process([parameters).ResultasCommerceEntity;

If you need to call those commands synchronously from a synchronous method, then this could be one way to do it:

varentity = Task.Runawait_createCatalogCommand.Process([parameters)).ResultasCommerceEntity;

Initializing Commerce commands with dependency injection.

All examples below take advantage of Commerce commands provided by Commerce SDK. Command instances would usually be injected into nesting class via its constructor. Alternatively commands can be passed directly into called methods or instantiated with CommerceCommander, which would instantiate needed Command instances using reflection. This blog post explains how to use CommerceCommander. We ll mostly use Dependency Injected commands, but will use CommerceCommander to call PersistEntityPipeline when saving changes to updated entity. All sample methods are hosted in a controller class, which instantiated like this:

public class CommandsController : CommerceController
{
   #region 
   private readonly CommerceEnvironment _globalEnvironment;
   private readonly FindEntityCommand _findEntityCommand;
   private readonly CreateCatalogCommand _createCatalogCommand;
   private readonly CreateCategoryCommand _createCategoryCommand;
   private readonly CreateSellableItemCommand _createSellableItemCommand;
   private readonly CreateSellableItemVariationCommand
        _createSellableItemVariantCommand;
   private readonly AddEntityVersionCommand _addEntityVersionCommand;
   private readonly AddPromotionBookCommand _addPromotionBookCommand;
   private readonly AddPriceBookCommand _addPriceBookCommand;
   #endregion
   public CommandsController(
     ?IServiceProvider serviceProvider,
     ?FindEntityCommand findEntityCommand,
     ?CommerceEnvironment globalEnvironment,
     ?CreateCatalogCommand createCatalogCommand,
     ?CreateCategoryCommand createCategoryCommand,
     ?CreateSellableItemCommand createSellableItemCommand,
     ?CreateSellableItemVariationCommand createSellableItemVariationCommand,
     ?AddPriceBookCommand addPriceBookCommand,
     ?AddPromotionBookCommand addPromotionBookCommand,
     ?AddEntityVersionCommand addEntityVersionCommand) :
        base(serviceProvider, globalEnvironment)
   {
     ?_findEntityCommand = findEntityCommand;
     ?_createSellableItemCommand = createSellableItemCommand;
     ?_globalEnvironment = globalEnvironment;
     ?_createCategoryCommand = createCategoryCommand;
     ?_addPriceBookCommand = addPriceBookCommand;
     ?_addPromotionBookCommand = addPromotionBookCommand;
     ?_createCatalogCommand = createCatalogCommand;
     ?_addEntityVersionCommand = addEntityVersionCommand;
     ?_createSellableItemVariantCommand = createSellableItemVariationCommand;
   }
}

Now with all prep work done in class constructor, let s jump to code snippets .


Create or update catalog

With CreateCatalogCommand new Ctaalog can be created with just one line of code:

var newCatalog = await _createCatalogCommand.Process(CurrentContext, catalogName, catalogName).Result as Catalog;

But in real-life scenarios we would first check if Catalog already exists before trying to create one and then Catalog would usually be associated with Price and Promotions Books, so below code snippet does all this:

private async Task GetOrCreateCatalog(string catalogName)
{
   //Commerce would use a add different prefixes to internal IDs of different kinds of entities.
   //this will get us internal commerce ID for a given catalog name 
   var commerceCatalogId = $"{CommerceEntity.IdPrefix()}{catalogName}";
 
   //Check if catalog with given name already exists before trying to create a new one
   var catalog = await _findEntityCommand.Process(CurrentContext, typeof(Catalog),
                                commerceCatalogId, false) as Catalog;
   if (catalog == null)
   {
     ?var catalogBaseName = catalogName.Replace("_Catalog", string.Empty);
     ?catalog = await _createCatalogCommand.Process(CurrentContext, catalogName, catalogName) as Catalog;
 
     ?//Find or create default Price Book for Catalog
     ?var pricebookname = catalogBaseName + "PriceBook";
     ?var pricebookId = $"{(object)CommerceEntity.IdPrefix()}{(object)pricebookname}";
     ?var pricebook = await _findEntityCommand.Process(CurrentContext, typeof(PriceBook),
                                     ?pricebookId, false) as PriceBook;
     ?if (pricebook == null)
     ?{
        var addPricebookCommand = Command();
        pricebook = await addPricebookCommand.Process(CurrentContext, catalogBaseName + "PriceBook",
           catalogBaseName + "PriceBook", catalogBaseName + " Book") as PriceBook;
     ?}
 
     ?//Find or create default Promotions Book for Catalog
     ?var promobookname = catalogBaseName + "PromotionsBook";
     ?var promobookId = $"{(object)CommerceEntity.IdPrefix()}{(object)promobookname}";
     ?var promobook = await _findEntityCommand.Process(CurrentContext, typeof(PromotionBook),
        promobookId, false) as PromotionBook;
     ?if (promobook == null)
     ?{
        var addPromobookCommand = Command();
        promobook = await addPromobookCommand.Process(CurrentContext, promobookname, promobookname,
           catalogBaseName + " Promotion Book", "") as PromotionBook;
     ?}
 
     ?//Associate Catalog with its default Price Book
     ?if (pricebook != null && !string.IsNullOrEmpty(pricebook.Name))
     ?{
        catalog.PriceBookName = pricebook.Name;
     ?}
 
     ?//Associate Catalog with its default Promotoins Book
     ?if (promobook != null && !string.IsNullOrEmpty(promobook.Name))
     ?{
        catalog.PromotionBookName = promobook.Name;
     ?}
 
     ?//Persist changes to Catalog (Price and Promo books associations created above) into Commerce database
     ?var result = await _commerceCommander.Pipeline()
           .Run(new PersistEntityArgument(catalog), this.CurrentContext.GetPipelineContextOptions());
     ?catalog = result.Entity as Catalog;
   }
 
   return catalog;
}

Create or Update Category

Let s say we need to update an existing category or create a new one if it doesn t exist. We will also create a new item version for given Category if it already exists and its state is not Draft (and if it s in Draft then just update its most current version). Category properties would be passed in CategoryModel object.

public async Task CreateOrUpdateCategory2Async(CategoryModel categoryModel)
{
   //Get Commerce IDs for given category and reolated entities
   var parentCategoryId = categoryModel.ParentCategoryId;
   var commerceCatalogId = $"{CommerceEntity.IdPrefix()}{categoryModel.CatalogName}";
   var commerceCategoryId = $"{CommerceEntity.IdPrefix()}{categoryModel.CatalogName}-{categoryModel.Id}";
 
   //Get Catalog by name or create a new one if not found
   var catalog = GetOrCreateCatalog(categoryModel.CatalogName);
 
   //Find category by ID
   var category = await _findEntityCommand.Process(this.CurrentContext, typeof(Category), commerceCategoryId) as Category;
   if (category == null)
   {
     ?//Create category if it don't already exist in Commerce Catalog
     ?category = await _createCategoryCommand.Process(CurrentContext, catalog.Id, categoryModel.Name,
        categoryModel.DisplayName, categoryModel.Description) as Category;
   }
   else
   {
     ?//Check Category workflow state, if Category is not in "Draft" state then don't update current version - create a new one and update that 
     ?var workflowComponent = category.GetComponent();
     ?if (workflowComponent != null)
     ?{
        if (workflowComponent.CurrentState != null && !workflowComponent.CurrentState.Equals("Draft", StringComparison.OrdinalIgnoreCase))
        {
           var newEntityVersion = category.EntityVersion + 1;
           var addVersionResult = await _addEntityVersionCommand.Process(this.CurrentContext, category, newEntityVersion);
 
           //Update Category reference to point to newly created item version
           category = await _findEntityCommand.Process(this.CurrentContext, typeof(Category),
             ?commerceCategoryId, entityVersion: newEntityVersion) as Category;
        }
     ?}
 
     ?//Update properties on existing Category
     ?category.Name = categoryModel.Name;
     ?category.DisplayName = categoryModel.DisplayName;
     ?category.Description = categoryModel.Description;
     ?//Save changes to to existing Category into Commeredce database
     ?var saveResult = _commerceCommander.Pipeline()
        .Run(new PersistEntityArgument(category),
        this.CurrentContext.GetPipelineContextOptions());
 
     ?category = saveResult.Result.Entity as Category;
   }
 
   return category;
}

Create or Update Sellable Item

Now moving down Catalog hierarchy let s review the process of creating/updating the Sellable Item, which usually is maps to a product that can be sold, hence the name Sellable Item . SellableItem entity can be created with this command

var sellableItem = await _createSellableItemCommand.Process(CurrentContext, productModel.Id, productModel.Name, productModel.DisplayName,
                productModel.Description, productModel.BrandName, productModel.Manufacturer, productModel.TypeOfGood, productModel.Tags);

In real-world scenarios, similarly to previous Category example, we would check if given Sellable Item already exist, update if it does, create a new one if it doesn t. Before updating properties of already existing Sellable Item we will check its Workflow State and create new Item Version of it s not in Draft mode.

public async Task CreateOrUpdateSellableItem(ProductModel productModel)
{
   //Get Commerce-friendly IDs of sellable item and related entities
   var commerceSellableItemId = $"{CommerceEntity.IdPrefix()}{productModel.Id}";
   var catalogCommerceId = $"{CommerceEntity.IdPrefix()}{productModel.CatalogName}";
   var commerceParentCatgoryId = $"{CommerceEntity.IdPrefix()}{productModel.CatalogName}-{productModel.CategoryId}";
 
   //Try to find given sellable item in Commerce database
   SellableItem sellableItem = await _findEntityCommand.Process(this.CurrentContext, typeof(SellableItem), commerceSellableItemId) as SellableItem;
   if (sellableItem == null)
   {
     ?//Create new Sellable Item, pass model properties as parameters
     ?sellableItem = await _createSellableItemCommand.Process(CurrentContext, productModel.Id, productModel.Name, productModel.DisplayName,
        productModel.Description, productModel.BrandName, productModel.Manufacturer, productModel.TypeOfGood, productModel.Tags);
   }
   else
   {
     ?//Check existing entity's workflow state and create a new item version if its workflow state is not in "Draft" 
     ?var workflowComponent = sellableItem.GetComponent();
     ?if (workflowComponent != null)
     ?{
        if (workflowComponent.CurrentState != null && !workflowComponent.CurrentState.Equals("Draft", StringComparison.OrdinalIgnoreCase))
        {
           var newItemVersion = sellableItem.EntityVersion + 1;
           await _addEntityVersionCommand.Process(this.CurrentContext, sellableItem, newItemVersion);
 
           //Set current sellableItem object to newly created version
           sellableItem = await _findEntityCommand.Process(this.CurrentContext, typeof(SellableItem), commerceSellableItemId, newItemVersion) as SellableItem;
        }
     ?}
     ?//Update Sellable Item properties
     ?sellableItem.Name = productModel.Name;
     ?sellableItem.DisplayName = productModel.DisplayName;
     ?sellableItem.Description = productModel.Description;
     ?sellableItem.Brand = productModel.BrandName;
     ?sellableItem.Manufacturer = productModel.Manufacturer;
     ?sellableItem.TypeOfGood = productModel.TypeOfGood;
     ?sellableItem.Tags = productModel.Tags;
 
     ?//Save changes to Commerce database
     ?await _commerceCommander.Pipeline()
        .Run(new PersistEntityArgument(sellableItem), this.CurrentContext.GetPipelineContextOptions());
   }
 
   return sellableItem;
}

Create or Update Sellable Item Variant

Sellable Item Variants are different from Categories and Sellable Items because they exist not as a separate entity, but as part of Sellable Item as are saved as such, as a component of SellableItem entity. In order to create or update SellableItemVariant component we need to do the following

  • Find parent Sellable Item
  • Update existing or create new SellableItemVariant component inside its parent SellableItem
  • Save changes to parent SellableItem entity
public async Task CreateOrUpdateSellableItemVariant(ProductVariationModel productVariationModel)
{
   //Get Commerce-friendly ID of the parent SellableItem entity
   var commerceParentSellableItemId = $"{CommerceEntity.IdPrefix()}{productVariationModel.ProductId}";
 
   //Find parent SellableItem in Commerce database
   SellableItem parentSellableItem = await _findEntityCommand.Process(this.CurrentContext, typeof(SellableItem),
     ?commerceParentSellableItemId) as SellableItem;
 
   //If SellableItem don't exist - create one
   if (parentSellableItem == null)
   {
     ?//Assuming we have GetProductModel method, which will return ProductModel for new SellableItem to be created from it
     ?var productModel = GetProductModel(productVariationModel.ProductId);
     ?//Create new Sellable Item with code descripbed above
     ?parentSellableItem = await CreateOrUpdateSellableItem(productModel);
   }
   else
   {
     ?//Check workflow state, create new item version if workflow is not in "Draft"
     ?var workflowComponent = parentSellableItem.GetComponent();
     ?if (workflowComponent != null)
     ?{
        if (workflowComponent.CurrentState != null && !workflowComponent.CurrentState.Equals("Draft", StringComparison.OrdinalIgnoreCase))
        {
           //Add new Entity Version to parent SellableItem entity
           var newEntityVersion = parentSellableItem.EntityVersion + 1;
           var addVersionResult = await _addEntityVersionCommand.Process(this.CurrentContext, parentSellableItem, newEntityVersion);
           parentSellableItem = await _findEntityCommand.Process(this.CurrentContext, typeof(SellableItem), commerceParentSellableItemId,
             ?entityVersion: newEntityVersion) as SellableItem;
        }
     ?}
   }
 
   //Retrieve Sellable Item Variant component from parent SellableItem
   var itemVariationComponent = parentSellableItem.GetComponent()
           .ChildComponents.FirstOrDefault(y => y.Id == productVariationModel.VariantId) as ItemVariationComponent;
   if (itemVariationComponent == null)
   {
     ?parentSellableItem = await _createSellableItemVariantCommand.Process(CurrentContext, commerceParentSellableItemId,
        productVariationModel.VariantId, productVariationModel.Name, productVariationModel.DisplayName);
     ?itemVariationComponent = parentSellableItem.GetComponent()
             ?.ChildComponents.FirstOrDefault(y => y.Id == productVariationModel.VariantId) as ItemVariationComponent;
   }
   else
   {
     ?//Populate Variant properties on existing entity
     ?itemVariationComponent.Name = productVariationModel.Name;
     ?itemVariationComponent.DisplayName = productVariationModel.DisplayName;
   }
 
   //Save Varianrt changes into parent SellableItem entity
   parentSellableItem.SetComponent(itemVariationComponent);
   await _commerceCommander.Pipeline()
     ?.Run(new PersistEntityArgument(parentSellableItem), this.CurrentContext.GetPipelineContextOptions());
 
   return itemVariationComponent;
}

Closing words

Hope this helps those who need a bit more detailed code samples in addition to Sitecore Experience Commerce documentation and many great posts on Sitecore Experience Commerce 9.

Related Blogs

Latest Blogs