allBlogsList

Updating Sitecore Commerce Composer Fields Programmatically

Introduction

This post shows a flexible way to update Sitecore Commerce Entity Composer fields programmatically based on matching fields names.

Entity Composer in Sitecore Commerce 9+

Sitecore Entity Composer is a great way to add custom fields to entity definitions in Sitecore Commerce 9.0.2 and later without having to write any code. This blog series provides a good overview of working with and extending Composer Templates. While writing my Commerce Connector for Content Hub I had to figure a flexible way to update these Composer fields programmatically; I needed incoming field values to be mapped to their corresponding composer fields using their field names, like so:

  • The incoming data from a 3-rd party source (Content Hub in my case) comes in the structure of type Dictionary<string, object> where the key is the field name and the value is the actual value.
  • Mapping configuration maps each incoming field to the destination field by field name, e.g. the value of incoming "field A" should be saved into "field B" of the target catalog entity, where "Field B" is defined in Commerce Composer template.

How Mapping Configuration works

Mapping configurations is a set of policies configured via their policy JSON configuration files, one for each entity type and incoming source. For the purposes of this post, I'll simplify mapping configuration as follows, just to give a general idea.

  • The incoming data is a Dictionary<string, object> where the key is the field name and the value is the actual field value. Let's say it looks like this:

    • fieldA: "value A: (string)
    • fieldB: 1 (int)
    • fieldC: true (bool)
  • The destination is, say, a SellableItem already associated with a couple of Composer templates like so:

    • "Composer Template 1"

      • "Composer Field A": string
    • "Composer Template 2"

      • "Composer Field B": int
      • "Composer Field C": checkbox
  • The mapping configuration, in this case, would look like this (Note that I'm omitting composer template names, the code below will iterate through all composer templates on the target items, looking for field match and then will try to convert data type from an incoming object into the data type of the target data field):

    • "fieldA" -> "Composer Field A"
    • "fieldB" -> "Composer Field A"
    • "fieldC" -> "Composer Field A"
  • Hopefully, the above explanation is not too confusing, but if it is, it's just the way I mapped incoming data to Commerce Catalog Entities. The code below can be modified to utilize different data structures for data coming into Commerce Catalog. The main purpose of this post is to show how to iterate through and update Commerce Entity Composer fields dynamically, converting incoming values into a proper data type to match the destination field defined with Commerce Entity Composer. I wanted to share what worked for me after a good deal of experimentation since I was unable to find good code samples for this particular use case in Sitecore documentation or anywhere on the Web.

And Now to the Code Part

Following Sitecore Commerce coding guidelines my custom code will reside in its own PipelineBlock, which I added to my custom SaveEntity pipeline. Here's how mine looks like, nothing really special so far...

    /// <summary>
    /// Update Composer Template fields on Commerce Entity
    /// </summary>
    [PipelineDisplayName("UpdateComposerFieldsBlock")]
    public class UpdateComposerFieldsBlock : PipelineBlock<ImportCatalogEntityArgument, ImportCatalogEntityArgument, CommercePipelineExecutionContext>
    {
        #region Private fields
        private readonly ComposerCommander _composerCommander;
        private readonly CommerceCommander _commerceCommander;
        private readonly CommerceEntityImportHelper _importHelper;
        #endregion
        #region Public methods
        /// <summary>
        /// Public contructor
        /// </summary>
        /// <param name="commerceCommander"></param>
        /// <param name="composerCommander"></param>
        /// <param name="importHelper"></param>
        public UpdateComposerFieldsBlock(CommerceCommander commerceCommander, ComposerCommander composerCommander)
        {
            _commerceCommander = commerceCommander;
            _composerCommander = composerCommander;
            _importHelper = new CommerceEntityImportHelper(commerceCommander, composerCommander);
        }
        ...

PipelineBlock must have the Run method present, which, among other things would call my custom private method, ImportComposerViewsFields.

public override async Task<ImportCatalogEntityArgument> Run(ImportCatalogEntityArgument arg, CommercePipelineExecutionContext context)
 {
	...
         await ImportComposerViewsFields(entity, entityDataModel.EntityFields, context.CommerceContext);
     ...
     return arg;
 }

This last piece is the most interesting part: it iterates through the input data collection, looks up matching fields in given CommerceEntity, updates matching fields, and then saves changes to the Commerce database.

private async Task<bool> ImportComposerViewsFields(CommerceEntity commerceEntity, Dictionary<string, string> entityFields, CommerceContext context)
{
    //Get root/master view of the target entity, composer views, if any, will be included in Child views of this master view
    var masterView = await _commerceCommander.Command<GetEntityViewCommand>().Process(
        context, 
        commerceEntity.Id,
        commerceEntity.EntityVersion,
        context.GetPolicy<KnownCatalogViewsPolicy>().Master,
        string.Empty,
        string.Empty);
 
    if (masterView == null)
    {
        Log.Error($"Master view not found on Commerce Entity, Entity ID={commerceEntity.Id}");
        throw new ApplicationException($"Master view not found on Commerce Entity, Entity ID={commerceEntity.Id}");
    }
 
    if (masterView.ChildViews == null || masterView.ChildViews.Count == 0)
    {
        Log.Error($"No composer-generated views found on Sellable Item entity, Entity ID={commerceEntity.Id}");
        throw new ApplicationException($"No composer-generated views found on Sellable Item entity, Entity ID={commerceEntity.Id}");
    }
 
    //Now iterate through child views and then their child fields, looking for matching names
    var isUpdated = false;
    foreach (EntityView view in masterView.ChildViews)
    {
        EntityView composerViewForEdit = null;
        foreach (var viewField in view.Properties)
        {
            //Found matching field that need to be updated
            if (entityFields.Keys.Contains(viewField.Name))
            {
                //Retrieve the composer view to update...
                if (composerViewForEdit == null)
                {
                    composerViewForEdit = Task.Run<EntityView>(async () => await commerceEntity.GetComposerView(view.ItemId, _commerceCommander, context)).Result;
                }
                //...and update the field value
                if (composerViewForEdit != null)
                {
                    var composerProperty = composerViewForEdit.GetProperty(viewField.Name);
                    if (composerViewForEdit != null)
                    {
                        composerProperty.ParseValueAndSetEntityView(entityFields[viewField.Name]);
                        isUpdated = true;
                    }
                }
            }
        }
    }
 
    //Save an updated entity if changes were made
    if (isUpdated)
    {
        return await _composerCommander.PersistEntity(context, commerceEntity);
    }
 
    return false;
}