allBlogsList

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 Items in Sitecore 9

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.Run<CommerceEntity&gt;(async() =>await_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 <Commerce Commands>
    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<Catalog> 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<Catalog>()}{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<PriceBook>()}{(object)pricebookname}";
        var pricebook = await _findEntityCommand.Process(CurrentContext, typeof(PriceBook),
                                                        pricebookId, false) as PriceBook;
        if (pricebook == null)
        {
            var addPricebookCommand = Command<AddPriceBookCommand>();
            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<PromotionBook>()}{(object)promobookname}";
        var promobook = await _findEntityCommand.Process(CurrentContext, typeof(PromotionBook),
            promobookId, false) as PromotionBook;
        if (promobook == null)
        {
            var addPromobookCommand = Command<AddPromotionBookCommand>();
            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<IPersistEntityPipeline>()
                .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<Category> CreateOrUpdateCategory2Async(CategoryModel categoryModel)
{
    //Get Commerce IDs for given category and reolated entities
    var parentCategoryId = categoryModel.ParentCategoryId;
    var commerceCatalogId = $"{CommerceEntity.IdPrefix<Catalog>()}{categoryModel.CatalogName}";
    var commerceCategoryId = $"{CommerceEntity.IdPrefix<Category>()}{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<WorkflowComponent>();
        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<IPersistEntityPipeline>()
            .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<SellableItem> CreateOrUpdateSellableItem(ProductModel productModel)
{
    //Get Commerce-friendly IDs of sellable item and related entities
    var commerceSellableItemId = $"{CommerceEntity.IdPrefix<SellableItem>()}{productModel.Id}";
    var catalogCommerceId = $"{CommerceEntity.IdPrefix<Catalog>()}{productModel.CatalogName}";
    var commerceParentCatgoryId = $"{CommerceEntity.IdPrefix<Category>()}{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<WorkflowComponent>();
        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<IPersistEntityPipeline>()
            .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<ItemVariationComponent> CreateOrUpdateSellableItemVariant(ProductVariationModel productVariationModel)
{
    //Get Commerce-friendly ID of the parent SellableItem entity
    var commerceParentSellableItemId = $"{CommerceEntity.IdPrefix<SellableItem>()}{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<WorkflowComponent>();
        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<ItemVariationsComponent>()
                .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<ItemVariationsComponent>()
                    .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<IPersistEntityPipeline>()
        .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.