Sitecore Experience Commerce: Pipeline Block Types

In this article, we will look at the foundational pipeline block types that are used to construct pipelines within the Commerce Engine plugins and custom solutions.

Asynchronous vs Synchronous Pipeline Blocks

From XC 10.0, the PipelineBlock class has been split into AsyncPipelineBlock and SyncPipelineBlock classes for improved performance and stability, with the primary implementation difference being whether to call the pipeline block asynchronously or not during pipeline execution.

A notable change to the AsyncPipelineBlock is that it uses the RunAsync method instead of the Run method to enforce that the return type is a Task<TOutput> and as an added reminder that the pipeline block is in fact asynchronously executed.

How Async and Sync Pipeline Blocks Work in the Same Pipeline

The initial questions I had when seeing the pipeline split were "how does this affect registering pipeline blocks to pipelines?" and "how does this affect the way in which pipelines are executed?". The good news here is that is doesn't affect either from an implementation standpoint.

From the registration perspective, both async and sync pipelines are registered as per XC 9.X registration mechanisms.

.AddPipeline<IMyPipeline, MyPipeline>(pipeline => pipeline
    .Add<MySyncPipelineBlock>()
    .Add<MyAsyncPipelineBlock>()
    .Add<MyAsyncConditionalPipelineBlock>()
    .Add<MyAsyncPolicyTriggerConditionalPipelineBlock>()
    .Add<MyConditionalPipelineBlock>()
    .Add<MyAsyncConditionalPipelineBlock>()
)

From the async/sync execution perspective, async pipeline blocks are awaited before continuing on to the following pipeline block, which ensures the output of a pipeline block is the input of the following pipeline block.

Specialised Pipeline Blocks

You may have seen a few types of enhanced pipeline blocks in XC 9.X+, being ConditionalPipelineBlock and PolicyTriggerConditionalPipelineBlock, which may have been better suited to your custom implementations over adding conditional statements to the begining of your Run/RunAsync method.

Conditional Pipeline Blocks

As indicated by their names, the ConditionalPipelineBlock and AsyncConditionalPipelineBlock classes will be conditionally executed. This is achieved by defining the BlockCondition predicate to be evaluated to determine if the Run/RunAsync method should be called.

Where the evaluated BlockCondition returns false, an alternate method called ContinueTask is executed instead of Run/RunAsync and is intended to simply return the arg or null.

Returning arg from ContinueTask considers the pipeline block as optional whereas returning null may consider the pipeline block as mandatory depending on how this output is handled in subsequent pipeline blocks or the pipeline itself.

Warning: Using the ContinueTask method to run alternate code logic may be a sign of a bad code smell, such as breaking the separation of concerns rule. Having 2 separate pipeline blocks with inverted predicates is the recommended approach.

public class MyConditionalPipelineBlock : ConditionalPipelineBlock<MyArgument, MyArgument, CommercePipelineExecutionContext>
{
    public MyConditionalPipelineBlock()
    {
        BlockCondition = obj => ((CommercePipelineExecutionContext)obj).CommerceContext.HasPolicy<MyCustomPolicy>();
    }

    public override MyArgument Run(MyArgument arg, CommercePipelineExecutionContext context)
    {
        Condition.Requires(arg, nameof(arg)).IsNotNull();

        // My code logic

        return arg;
    }

    public override MyArgument ContinueTask(MyArgument arg, CommercePipelineExecutionContext context)
    {
        return arg;
    }
}

Policy Trigger Conditional Pipeline Blocks

Another more specialised form of conditional pipeline blocks are the PolicyTriggerConditionalPipelineBlock and AsyncPolicyTriggerConditionalPipelineBlock. These blocks actually have somewhat inverted logic to their name as you will need to specify the name of a policy in the ShouldNotRunPolicyTrigger property which will ensure the pipeline is not executed.

When the policy name is present in the request header's PolicyKeys value the pipeline block will execute the ContinueTask method over the Run/RunAsync method (See Conditional Pipeline Blocks for more details).

A full set of existing policy keys and their usages can be found in Commerce Developer Reference: Policy keys. It is recommended that the Commerce Engine policy keys be avoided when creating custom policy trigger conditional pipeline blocks.

The BlockCondition predicate is already implemented to manage the conditional logic as mentioned above and the ContinueTask method will return the arg as policy trigger conditional pipeline blocks should be considered optional and not mandatory.

public class MyPolicyTriggerConditionalPipelineBlock : PolicyTriggerConditionalPipelineBlock<MyArgument, MyArgument, CommercePipelineExecutionContext>
{
    public override string ShouldNotRunPolicyTrigger => "IgnoreMyPipelineBlock";

    public override MyArgument Run(MyArgument arg, CommercePipelineExecutionContext context)
    {
        Condition.Requires(arg, nameof(arg)).IsNotNull();

        // My code logic

        return arg;
    }
}

References

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);
	}
}