Sitecore Experience Commerce: Methods for Logging and Command Messaging

In this article, we will look at the APIs available for logging to the logging framework and applying command messages to the CommerceContext.

The reason for grouping these two subjects together is due to seeing a lot of confusion around these areas when reviewing developers' code in the field; there is some overlap between them, which is often overlooked.

In Sitecore Commerce, logging is based on Microsoft.Extensions.Logging, and the Sitecore Commerce Engine SDK is setup to utilise the SeriLog diagnostic library for logging.

The CommandMessages are flushed to calling CommerceCommands via the completion of the CommandActivity and are included in the response object of CommandsController APIs.

Logging and command messaging occurs within methods of the CommercePipelineExecutionContext and the CommerceContext.

CommercePipelineExecutionContext

LogInfoIf

public void LogInfoIf(bool conditionResult, string info);

Intuitive enough, the LogInfoIf method will log an Information level entry, info, if the conditionResult is met.

Abort

public override void Abort(string reason, object data);

The Abort method will abort the pipeline and will create an Error level log entry if the reason message doesn't contain the magic string "Ok|".

It's also worth noting that this method is intended to abort the executing pipeline first and foremost, and the log entry is secondary. It is not intended solely for the purpose of logging.

Note: The data object would normally return the current CommercePipelineExecutionContext.

CommerceContext

AddDataMessage

public virtual void AddDataMessage(string messageType, string dataMessage);

The AddDataMessage will add the dataMessage to the command messages. It will also add the dataMessage to the logger at the Information level.

Tip: Avoid setting Debug level messages to avoid spamming your local development logs, which are defaulted to the Information level.

AddMessage

public virtual void AddMessage(CommandMessage message);

AddMessage will add a CommandMessage to the command messages.

The message will not invoke the logger.

AddMessage (Alternate)

public virtual Task<string> AddMessage(string code, string commerceTermKey, object[] args, string defaultMessage = null);

The overloaded AddMessage method's logging behaviour is as follows:

  • Message codes of ValidationError or Warning will add a Warning message to the Logger.
  • All exception types will be logged using the LogException method. See LogException for more details.
  • An Error will also be logged as an Error.

For the CommandMessages, the localised message will attempted to be retrieved from Sitecore, using the commerce term key provided, and further formatted/interpolated with the args provided.

LogException

public virtual void LogException(string caller, Exception ex);

The LogException method will log an exception as an error with the exception message and stack trace details.

LogExceptionAndMessage

public virtual void LogExceptionAndMessage(string caller, Exception ex);

The LogExceptionAndMessage logs the exception as per the LogException method, however the exception message will be added to the CommandMessages at the Error level in addition.

Logger

public ILogger Logger { get; }

The Logger exposes the can be utilised to add the standard log-level entries to it, being:

  • LogDebug
  • LogInformation
  • LogWarning
  • LogError
  • LogCritical

Sitecore Experience Commerce: Conditionally Executing Pipeline Blocks

In this article, we will look at the various approaches to implementing pipeline blocks to conditionally execute.

As the pipeline framework executes pipeline blocks linearly, there may be instances where pipeline blocks registered to the pipeline should not be executed. For example, in the following pipeline both the Braintree and Gift Cards payments are registered in the IProcessPaymentsPipeline to capture payments, however the customer may have only paid with a single payment method.

IProcessPaymentsPipeline
  Sample.Payments.Braintree.CapturePaymentsBlock 
  Sample.Payments.Finance.CapturePaymentsBlock  
  Sample.Payments.GiftCards.CapturePaymentsBlock 

To prevent unnecessary execution of pipeline blocks, the following approaches are discussed generically and not specific to the sample pipeline above.

The Pipeline Block

The PipelineBlock is the common abstract class that the majority of pipeline blocks are based off of. The Run method is where the business logic is placed and will be executed for the pipeline block. There is no mechanism to conditionally execute the business logic within its Run method, however we can simply achieve this by returning the pipeline early if a certain condition is not met.

The following example, simply checks to see if the cart has a FederatedPaymentComponent, otherwise it will return the pipeline argument to continue execution of the pipeline.

public class SamplePipelineBlock : PipelineBlock<SamplePipelineArgument, SamplePipelineArgument, CommercePipelineExecutionContext>
{
	protected CommerceCommander Commander { get; set; }

	public SamplePipelineBlock(CommerceCommander commander)
	: base(null)
	{
		this.Commander = commander;
	}
		
	public override async Task<SamplePipelineArgument> Run(SamplePipelineArgument arg, CommercePipelineExecutionContext context)
	{
		Condition.Requires(arg).IsNotNull($"{this.Name}: The argument can not be null");

		var cart = arg.Cart;
		if (!cart.HasComponent<FederatedPaymentComponent>())
		{
			return await Task.FromResult(arg).ConfigureAwait(false);
		}

		/* Add business logic here */

		return await Task.FromResult(arg).ConfigureAwait(false);
	}
}

The ConditionalPipelineBlock

The ConditionalPipelineBlock is an abstract class that allows a custom pipeline block to be implemented with the BlockCondition Predicate, which will determine whether to execute the Run method, otherwise executing the ContinueTask method.

In Sitecore Commerce Project projects, this pipeline block implements concrete pipeline block by utilising the Run method as the standard housing for the business logic; the BlockCondition is implemented to determine whether a policy is available within the current environment or global environment, dependent on the context, and the ContinueTask to return the pipeline argument to continue the pipeline.

Note: The BlockCondition does not be determined by an environment policy, however consider whether another pipeline block approach may be more beneficial if implementing conditional logic alternate to an environment policy.

Note: It is strongly recommended that the ContinueTask simply returns the pipeline argument, but it is not mandatory. Consider alternative approaches, such as additional confitional pipeline blocks, prior to adding business logic in this method.

public class SampleConditionalPipelineBlock: ConditionalPipelineBlock<SamplePipelineArgument, SamplePipelineArgument, CommercePipelineExecutionContext>
{
	public SampleConditionalPipelineBlock()
	{
		this.BlockCondition = ValidatePolicy;
	}

	private static bool ValidatePolicy(IPipelineExecutionContext context)
	{
		return ((CommercePipelineExecutionContext)context).CommerceContext.HasPolicy<SampleEnvironmentPolicy>();
	}

	public override Task<SamplePipelineArgument> Run(SamplePipelineArgument arg, CommercePipelineExecutionContext context)
	{
		Condition.Requires(arg).IsNotNull($"{this.Name}: argument can not be null.");

		/* business logic here */

		return Task.FromResult(arg);
	}

	public override Task<SamplePipelineArgument> ContinueTask(SamplePipelineArgument arg, CommercePipelineExecutionContext context)
	{
		return Task.FromResult(arg);
	}
}

The PolicyTriggerConditionalPipelineBlock

The PolicyTriggerConditionalPipelineBlock is another abstract class, piggybacking off of the ConditionalPipelineBlock, introducing an abstract string ShouldNotRunPolicyTrigger, implementing the ContinueTask to return the pipeline argument my default, and implementing the BlockCondition to determine if a the pipeline block should run based on the ShouldNotRunPolicyTrigger value being present in the PolicyKeys request header property.

Tip: This approach may come in handy in custom integration import/export APIs.

public class SamplePolicyTriggerConditionalPipelineBlock : PolicyTriggerConditionalPipelineBlock<SamplePipelineArgument, SamplePipelineArgument>
{
	public override string ShouldNotRunPolicyTrigger
	{
		get
		{
			return "IgnoreSample";
		}
	}

	public override Task<SamplePipelineArgument> Run(SamplePipelineArgument arg, SamplePipelineArgument context)
	{
		Condition.Requires(arg).IsNotNull(this.Name + ": argument cannot be null.");

		/* business logic here */

		return Task.FromResult(arg);
	}
}

Solr Cheat Sheet for Sitecore Commerce Catalog Items

In this article, we will look at some of the ways in which we can query Solr for commerce catalog items in the Sitecore Master and Web indexes, and the Commerce CatalogItemsScope index.

Catalog Documents

Master and Web Indexes

Query ByQuery String
Namecommercesearchitemtype_t:"Catalog" && name_t:"<name>"
Display Namecommercesearchitemtype_t:"Catalog" && _displayname:"<display name>"
Entity Idcatalogentityid_t:"Entity-Catalog-<catalog entity id>"
Sitecore Idsitecoreid_t:"<Sitecore id>"
OR
_group:"<squashed Sitecore id>"

Commerce CatalogItemsScope Index

Query ByQuery String
Nameentityid:Entity-Catalog-* && name:"<name>"
Display Nameentityid:Entity-Catalog-* && displayname:"<display name>"
Entity Identityid:"Entity-Catalog-<friendly id>"
OR
entityid:"<entity id>"
Sitecore Idsitecoreid:"<Sitecore id>"

Category Documents

Master and Web Indexes

Query ByQuery String
Namecommercesearchitemtype_t:"Category" && _name:"<name>"
Display Namecommercesearchitemtype_t:"Category" && _displayname:"<display name>"
Entity Idcatalogentityid_t:"Entity-Category-<entity id>"
Sitecore Idsitecoreid_t:"<Sitecore id>"
OR
_group:"<squashed Sitecore id>"

Commerce CatalogItemsScope Index

Query ByQuery String
Nameentityid:Entity-Category-* && name:"<name>"
Display Nameentityid:Entity-Category-* && displayname:"<display name>"
Entity Identityid:"Entity-Category-<friendly id>"
OR
entityid:"<entity id>"
Sitecore Idsitecoreid:"<Sitecore id>"

Sellable Item Documents

Master and Web Indexes

Query ByQuery String
Namecommercesearchitemtype_t:"SellableItem" && _name:"<name>"
Display Namecommercesearchitemtype_t:"SellableItem" && _displayname:"<display name>"
Entity Idcatalogentityid_t:"Entity-SellableItem-<entity id>"
OR
productid_t:"<entity id>"
Sitecore Id_group:<Sitecore id>

Commerce CatalogItemsScope Index

Query ByQuery String
Nameentityid:Entity-SellableItem-* && name:<name>"
Display Nameentityid:Entity-SellableItem-* && displayname:"<display name>"
Entity Identityid:"Entity-SellableItem-<friendly id>"
OR
entityid:"<entity id>"
Variant Idvariantid:"<variant id>"
Variant Display Namevariantdisplayname:*<variant display name>*
Sitecore Idsitecoreid:"<Sitecore id>"