This follows on from creating a catalog programmatically, and creating categories programmatically.

Firstly, let’s look at why you might want to do this. There are several good reasons, such as: Early in a project, you want to get started developing, but an ERP API isn’t available. It’s handy for generating some product data while learning Sitecore Commerce. There is no ERP. This is the case in our current project where there will never be an ERP supplying catalog information to Sitecore Commerce. Product information will be managed using a combination of BizTools and a spreadsheet import/export (which this will form the basis of). Benefits: The developer can completely control the catalog, categories and products. You can quickly iterate custom components, cleaning the database and redeploying new products with your changes. You can quickly get your catalog data to a consistent state, which may help with testing. (You could also do this with a catalog export.)

Downsides: It is up to developers to maintain the product data. If the catalog is complex, it could eat up a lot of time. Most of this work will probably be thrown away once you have an ERP or import process created.

In line with the downsides above, be careful about spending too much time creating product data vs creating the data structures that your project needs.

OK so here’s the code. In keeping with the liquor theme from the categories, we’re creating a bottle of red wine as a sellable item, and several variants for the different sizes available.

InitializeSellableItemsBlock.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Core.Commands;
using Sitecore.Commerce.Plugin.Catalog;
using Sitecore.Commerce.Plugin.Pricing;
using Sitecore.Framework.Pipelines;

namespace MyProject.Plugin.Catalog.Pipelines.Blocks.SeedCatalog
{
    [PipelineDisplayName("MyProject.Block.InitializeSellableItemsBlock")]
    public class InitializeSellableItemsBlock : PipelineBlock<string, string, CommercePipelineExecutionContext>
    {
        private readonly CommerceCommander _commerceCommander;

        public InitializeSellableItemsBlock(CommerceCommander commerceCommander)
        {
            _commerceCommander = commerceCommander;
        }

        public override async Task<string> Run(string arg, CommercePipelineExecutionContext context)
        {
            //Check the environment to make sure we're only running this where we want to.
            if (arg != "MyProjectAuthoring")
            {
                return arg;
            }

            var catalog = context.CommerceContext.GetObject<Sitecore.Commerce.Plugin.Catalog.Catalog>();

            if (catalog == null)
            {
                context.Logger.LogError("Could not find catalog");
            }

            //Create a sample product to put in our catalog.
            var product = new BootstrapProduct()
            {
                Id = "1000",
                Name = "Old Mate's Shiraz",
                CategoryName = "Shiraz",
                ListPrice = 13.95m,
                Variants = new List<BootstrapVariant>()
                {
                    new BootstrapVariant()
                    {
                        Id = "1000_375",
                        Name = "Old Mate's Shiraz - 375mL",
                        ListPrice = 18.95m,
                        Size = "375mL"
                    },
                    new BootstrapVariant()
                    {
                        Id = "1000_750",
                        Name = "Old Mate's Shiraz - 750mL",
                        ListPrice = 30.95m,
                        Size = "750mL"
                    },
                    new BootstrapVariant()
                    {
                        Id = "1000_1000",
                        Name = "Old Mate's Shiraz - 1L",
                        ListPrice = 32.95m,
                        Size = "1L"
                    }
                }
            };

            await BootstrapSellableItem(product, catalog, context);

            return arg;
        }
        
        /// <summary>
        /// Creates a new sellable item and any variants in Sitecore Commerce.
        /// </summary>
        /// <param name="productData">The BootstrapProduct containing the data to be saved into Commerce.</param>
        /// <param name="catalog">The catalog to add the data to.</param>
        /// <param name="context">The commerce context that we are talking to.</param>
        /// <returns></returns>
        private async Task BootstrapSellableItem(BootstrapProduct productData, Sitecore.Commerce.Plugin.Catalog.Catalog catalog, 
            CommercePipelineExecutionContext context)
        {
            //Create a new sellable item in the catalog.
            var sellableItem = await _commerceCommander.Command<CreateSellableItemCommand>()
                .Process(context.CommerceContext, productData.Id, productData.Name, productData.Name, string.Empty);
            if (sellableItem == null)
            {
                context.Logger.LogError($"Sellable item, {productData.Id} | {productData.Name}, was not created.");
            }

            //For convenience, we'll grab the new sellable item's ID.
            var actualSellableItemId = sellableItem.Id;

            //Get the category that the new sellable item will belong to.
            var category = await _commerceCommander.Command<GetCategoryCommand>()
                .Process(context.CommerceContext, productData.CategoryName.ToCategoryFriendlyId(catalog.Name));
            //Add the new sellable item to the category
            var referenceArgument = await _commerceCommander.Command<AssociateSellableItemToParentCommand>()
                .Process(context.CommerceContext, catalog.Id, category.Id, actualSellableItemId);

            // Get updated sellableitem after association for latest version
            sellableItem = await _commerceCommander.Command<FindEntityCommand>()
                .Process(context.CommerceContext, typeof(SellableItem), actualSellableItemId) as SellableItem;
            
            //Add a list price component to the sellable item.
            //Create more of these for any other components or custom components to be added.
            AddListPrice(productData, sellableItem);

            //Persist the changes.
            await _commerceCommander.PersistEntity(context.CommerceContext, sellableItem);

            //Add variants to the sellable item.
            foreach (var variantData in productData.Variants)
            {
                await AddVariant(variantData, sellableItem, context);
            }
        }

        /// <summary>
        /// Creates a new variant on a sellable item using data from a BootstrapVariant.
        /// </summary>
        /// <param name="variantData">The BootstrapVariant containing the data to be saved into Commerce.</param>
        /// <param name="sellableItem">The sellable item containing to add the variant to.</param>
        /// <param name="context">The commerce context that we are talking to.</param>
        /// <returns></returns>
        private async Task AddVariant(BootstrapVariant variantData, SellableItem sellableItem, CommercePipelineExecutionContext context)
        {
            //Create the new variant.
            sellableItem = await _commerceCommander.Command<CreateSellableItemVariationCommand>()
                .Process(context.CommerceContext, sellableItem.Id, variantData.Id, variantData.Name, variantData.Name);

            //Get the new variant from the sellable item.
            var variation = sellableItem.GetVariation(variantData.Id);
            if (variation == null)
            {
                context.Logger.LogError($"Could not find variant {variantData.Id} in sellable item {sellableItem.Id}");
                return;
            }

            //Add a list price component to the sellable item.
            //Create more of these for any other components or custom components to be added.
            AddListPrice(variantData, variation);
            //Update the display properties component.
            AddDisplayPropertiesToVariant(sellableItem, variantData.Id, variantData);

            //Save the changes
            await _commerceCommander.PersistEntity(context.CommerceContext, sellableItem);
        }

        /// <summary>
        /// Set values in the DisplayProperties component
        /// </summary>
        /// <param name="sellableItem">The sellable item containing the variant containing the displayproperties component.</param>
        /// <param name="variantId">The variant ID of the variant we want to update.</param>
        /// <param name="variantData">The variant data containing the new info.</param>
        private void AddDisplayPropertiesToVariant(SellableItem sellableItem, string variantId, BootstrapVariant variantData)
        {
            var displayProperties = sellableItem.GetComponent<DisplayPropertiesComponent>(variantId, false);
            displayProperties.Size = variantData.Size;
        }

        /// <summary>
        /// Add a list price to a sellable item.
        /// </summary>
        /// <param name="product">The bootstrap product with the price data.</param>
        /// <param name="item">The sellable item where we will update the list price.</param>
        private void AddListPrice(BootstrapProduct product, SellableItem item)
        {
            //only set the list price if we have one.
            if (product.ListPrice == 0)
            {
                return;
            }

            var listPricingPolicy = item.GetPolicy<ListPricingPolicy>();
            listPricingPolicy.AddPrice(new Money("USD", product.ListPrice));
        }

        /// <summary>
        /// Add a list price to a variant.
        /// </summary>
        /// <param name="variantData">The bootstrap variant with the price data.</param>
        /// <param name="variant">The variant component to be updated.</param>
        private void AddListPrice(BootstrapVariant variantData, ItemVariationComponent variant)
        {
            if (variantData.ListPrice == 0)
            {
                return;
            }

            var listPricingPolicy = variant.GetPolicy<ListPricingPolicy>();
            listPricingPolicy.AddPrice(new Money("USD", variantData.ListPrice));
        }

    }

    /// <summary>
    /// Bootstrap product that we can use to build product data to be saved into Commerce.
    /// </summary>
    public class BootstrapProduct
    {
        public string Id { get; set; }

        public string Name { get; set; }

        public decimal ListPrice { get; set; }

        public string CategoryName { get; set; }

        public List<BootstrapVariant> Variants { get; set; } = new List<BootstrapVariant>();
    }

    /// <summary>
    /// Bootstrap variant that we can use to build variants to save into Commerce.
    /// </summary>
    public class BootstrapVariant
    {
        public string Id { get; set; }

        public string Name { get; set; }

        public decimal ListPrice { get; set; }

        public string Size { get; set; }
    }
}

Once you’ve got your BootstrapSellableItems class built, you’ll need to add it to ConfigureSitecore.

ConfigureSitecore.cs

using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Sitecore.Commerce.Core;
using Sitecore.Framework.Configuration;
using Sitecore.Framework.Pipelines.Definitions.Extensions;
using MyProject.Plugin.Catalog.Pipelines.Blocks.SeedCatalog;

namespace MyProject.Plugin.Catalog
{
    public class ConfigureSitecore : IConfigureSitecore
    {
        public void ConfigureServices(IServiceCollection services)
        {
            var assembly = Assembly.GetExecutingAssembly();
            services.RegisterAllPipelineBlocks(assembly);

            services.Sitecore().Pipelines(x => x
                .ConfigurePipeline<IInitializeEnvironmentPipeline>(c => c
                    //.Add<InitializeCatalogBlock>() //see creating a catalog post
                    //.Add<InitializeCategoriesBlock>() //see creating categories post
                    .Add<InitializeSellableItemsBlock>()
                )
            );
        }
    }
}

Once that’s done, you should be able to run CleanEnvironment() and InitializeEnvironment() from postman to execute it.

Thanks to Andrew Sutherland from Sitecore for help in getting the right pipelines and other things in order.