How to extend Catalog system entities schema in Sitecore Experience Commerce 9.0

  • Description

    Sitecore Experience Commerce 9 provides a default schema for Sellable Items (Products), Categories and Catalogs. The following article describes how you extend default schema programmatically.

    A Business Tools UI mechanism for extending the schema will be available in a future update.

  • This section describes how you extend the Sellable Item entities through components. You can extend the other catalog system entities in a similar way.

    For more details on the Commerce plugin, refer to the "Creating your first plugin" section in the Sitecore Experience Commerce Developer's Guide.

    The code snippets referenced in this article are included in the Plugin.Sample.Notes.zip sample project.

    To extend the Sellable Item entity:

    Create a new component class with the properties that will be persisted as a part of the Sellable Item:

    namespace Plugin.Sample.Notes
    {
      using Sitecore.Commerce.Core;
     
      public class NotesComponents : Component
      {
        public string WarrantyInformation { get; set; } = string.Empty;
        public string InternalNotes { get; set; } = string.Empty;
      }
    }

    To allow users to provide content for these properties, the existing views for sellable items and their variants can be extended by creating a new entity view and registering it in the IGetEntityViewPipeline.

    When creating entity views, you should keep in mind that all blocks in that pipeline execute when an entity view is requested, so it is your responsibility to make sure that your block only acts for specific view names, action names or entity types.

    namespace Plugin.Sample.Notes
    {
      using System;
      using System.Threading.Tasks;
      using Sitecore.Commerce.Core;
      using Sitecore.Commerce.EntityViews;
      using Sitecore.Commerce.Plugin.Catalog;
      using Sitecore.Framework.Conditions;
      using Sitecore.Framework.Pipelines;
     
      [PipelineDisplayName(NotesConstants.Pipelines.Blocks.GetNotesViewBlock)]
      public class GetNotesViewBlock : PipelineBlock<EntityView, EntityView, CommercePipelineExecutionContext>
      {
        private readonly ViewCommander _viewCommander;
     
        public GetNotesViewBlock(ViewCommander viewCommander)
        {
          this._viewCommander = viewCommander;
        }
     
        public override Task<EntityView> Run(EntityView arg, CommercePipelineExecutionContext context)
        {
          Condition.Requires(arg).IsNotNull($"{Name}: The argument cannot be null.");
          var request = this._viewCommander.CurrentEntityViewArgument(context.CommerceContext);
          var catalogViewsPolicy = context.GetPolicy<KnownCatalogViewsPolicy>();
          var notesViewsPolicy = context.GetPolicy<KnownNotesViewsPolicy>();
          var notesActionsPolicy = context.GetPolicy<KnownNotesActionsPolicy>();
          var isVariationView = request.ViewName.Equals(catalogViewsPolicy.Variant, StringComparison.OrdinalIgnoreCase);
          var isConnectView = arg.Name.Equals(catalogViewsPolicy.ConnectSellableItem, StringComparison.OrdinalIgnoreCase);
     
          // Make sure that we target the correct views
          if (!isCatalogView && !isConnectView)
          {
            return Task.FromResult(arg);
          }
     
          // Only proceed if the current entity is a sellable item
          if (!(request.Entity is SellableItem))
          {
            return Task.FromResult(arg);
          }
     
          var sellableItem = (SellableItem)request.Entity;
     
          // See if we are dealing with the base sellable item or one of its variations.
          var variationId = string.Empty;
          if (isVariationView && !string.IsNullOrEmpty(arg.ItemId))
          {
            variationId = arg.ItemId;
          }
     
          var targetView = arg;
     
          // Check if the edit action was requested
          var isEditView =
          !string.IsNullOrEmpty(arg.Action) &&
          arg.Action.Equals(
            notesActionsPolicy.EditNotes,
            StringComparison.OrdinalIgnoreCase);
     
          if (!isEditView)
          {
            // Create a new view and add it to the current entity view.
            var view = new EntityView
            {
              Name = context.GetPolicy<KnownNotesViewsPolicy>().Notes,
              DisplayName = "Notes",
              EntityId = arg.EntityId,
              ItemId = variationId
            };
     
            arg.ChildViews.Add(view);
     
            targetView = view;
            }
     
          if (sellableItem != null && (sellableItem.HasComponent<NotesComponents>(variationId) || isConnectView || isEditView))
            {
              var component = sellableItem.GetComponent<NotesComponents>(variationId);
     
              targetView.Properties.Add(
              new ViewProperty
              {
                Name = nameof(NotesComponents.WarrantyInformation),
                RawValue = component.WarrantyInformation,
                IsReadOnly = !isEditView,
                IsRequired = false
              });
     
              // Add additional properties
          }
          return Task.FromResult(arg);
        }
      }
    }

    The sample above handles the Sellable Item views and adds the properties from the previously created component as view properties that are rendered in the business tools.

    If you want your content to be accessible in your storefront, you must handle the ConnectSellableItem view as shown above, as this allows the template generation to pick up new views.

    You must then create an action that takes the user input and persists it in the component of the given Sellable Item:

    namespace Plugin.Sample.Notes
    {
      using System;
      using System.Linq;
      using System.Threading.Tasks;
      using Sitecore.Commerce.Core;
      using Sitecore.Commerce.EntityViews;
      using Sitecore.Commerce.Plugin.Catalog;
      using Sitecore.Framework.Conditions;
      using Sitecore.Framework.Pipelines;
     
      [PipelineDisplayName(NotesConstants.Pipelines.Blocks.DoActionEditNotesBlock)]
      public class DoActionEditNotesBlock : PipelineBlock<EntityView, EntityView, CommercePipelineExecutionContext>
      {
        private readonly CommerceCommander _commerceCommander;
     
        public DoActionEditNotesBlock(CommerceCommander commerceCommander)
        {
          this._commerceCommander = commerceCommander;
        }
     
        public override Task<EntityView> Run(EntityView arg, CommercePipelineExecutionContext context)
        {
          Condition.Requires(arg).IsNotNull($"{Name}: The argument cannot be null.");
          var notesActionsPolicy = context.GetPolicy<KnownNotesActionsPolicy>();
    
          // Only proceed if the right action was invoked
          if (string.IsNullOrEmpty(arg.Action) || !arg.Action.Equals(notesActionsPolicy.EditNotes, StringComparison.OrdinalIgnoreCase))
          {
            return Task.FromResult(arg);
          }
     
          // Get the sellable item from the context
          var entity = context.CommerceContext.GetObject<SellableItem>(x => x.Id.Equals(arg.EntityId));
          if (entity == null)
          {
            return Task.FromResult(arg);
          }
     
          // Get the notes component from the sellable item or its variation
          var component = entity.GetComponent<NotesComponents>(arg.ItemId);
     
          // Map entity view properties to component
          component.WarrantyInformation = arg.Properties.FirstOrDefault(x => x.Name.Equals(nameof(NotesComponents.WarrantyInformation), StringComparison.OrdinalIgnoreCase))?.Value;
          component.InternalNotes = arg.Properties.FirstOrDefault(x => x.Name.Equals(nameof(NotesComponents.InternalNotes), StringComparison.OrdinalIgnoreCase))?.Value;
     
          // Persist changes
          this._commerceCommander.Pipeline<IPersistEntityPipeline>().Run(new PersistEntityArgument(entity), context);
     
          return Task.FromResult(arg);
        }
      }
    }

    Depending on the data that you are handling here, it is highly recommended that user input is validated before persisting any changes.

    You now have two blocks in place that can render content in the business tools and persist changes to the entities.

    In order to allow users to edit the view content from within the business tool, you must populate the view actions:

    namespace Plugin.Sample.Notes
    {
      using System;
      using System.Threading.Tasks;
      using Sitecore.Commerce.Core;
      using Sitecore.Commerce.EntityViews;
      using Sitecore.Framework.Conditions;
      using Sitecore.Framework.Pipelines;
     
      [PipelineDisplayName(NotesConstants.Pipelines.Blocks.PopulateNotesActionsBlock)]
      public class PopulateNotesActionsBlock : PipelineBlock<EntityView, EntityView, CommercePipelineExecutionContext>
      {
        public override Task<EntityView> Run(EntityView arg, CommercePipelineExecutionContext context)
        {
          Condition.Requires(arg).IsNotNull($"{Name}: The argument cannot be null.");
          var viewsPolicy = context.GetPolicy<KnownNotesViewsPolicy>();
     
          if (string.IsNullOrEmpty(arg?.Name) || !arg.Name.Equals(viewsPolicy.Notes, StringComparison.OrdinalIgnoreCase))
          {
            return Task.FromResult(arg);
          }
     
          var actionPolicy = arg.GetPolicy<ActionsPolicy>();
     
          actionPolicy.Actions.Add(
            new EntityActionView
            {
              Name = context.GetPolicy<KnownNotesActionsPolicy>().EditNotes,
              DisplayName = "Edit Sellable Item Notes",
              Description = "Edits the sellable item notes",
              IsEnabled = true,
              EntityView = arg.Name,
              Icon = "edit"
            });
     
          return Task.FromResult(arg);
        }
      }
    }

    In the last step, all the different blocks wire in to their respective pipelines. The pipeline configuration takes place in the ConfigureSitecore class of your plugin:

    namespace Plugin.Sample.Notes
    {
      using System.Reflection;
      using Microsoft.Extensions.DependencyInjection;
      using Sitecore.Commerce.Core;
      using Sitecore.Commerce.EntityViews;
      using Sitecore.Commerce.Plugin.Catalog;
      using Sitecore.Framework.Configuration;
      using Sitecore.Framework.Pipelines.Definitions.Extensions;
     
      public class ConfigureSitecore : IConfigureSitecore
      {
        public void ConfigureServices(IServiceCollection services)
        {
          var assembly = Assembly.GetExecutingAssembly();
          services.RegisterAllPipelineBlocks(assembly);
     
            services.Sitecore().Pipelines(config => config
              .ConfigurePipeline<IGetEntityViewPipeline>(c =>
              {
                c.Add<GetNotesViewBlock>().After<GetSellableItemDetailsViewBlock>();
              })
              .ConfigurePipeline<IPopulateEntityViewActionsPipeline>(c =>
              {
                c.Add<PopulateNotesActionsBlock>().After<InitializeEntityViewActionsBlock>();
              })
              .ConfigurePipeline<IDoActionPipeline>(c =>
              {
                c.Add<DoActionEditNotesBlock>().After<ValidateEntityVersionBlock>();
              })
          );
        }
      }
    }

    If you want to make your new properties available in your Storefront and you handled the Connect-specific views as mentioned above, you can open the Content Editor of your Sitecore XP instance and select the "Update Data Templates" command from the Commerce ribbon.

February 06, 2018
February 06, 2018