Dev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

Extend Cart v2

How to extend the Cart v2 asynchronous pipelines in Optimizely Configured Commerce.

In Cart v1, you extend the cart by adding a handler to a handler chain (see Add handlers). Cart v2 replaces the IHandler chain with an IAsyncPipeline, so the extension point is different. Instead of registering a new handler, you implement an extension that modifies the sequence of steps in an existing pipeline.

For background on why Cart v2 exists and how the pipeline interfaces fit together, see Cart v2 overview.

How extending differs from Cart v1

A Cart v1 handler and a Cart v2 extension achieve the same goal, but the mechanics differ in several important ways.

ConcernCart v1 (IHandler)Cart v2 (IAsyncPipelineExtension)
What you writeA new handler class added to the chainAn extension class that rewrites the existing step sequence
Base typeHandlerBase<TIn, TOut>IAsyncPipelineExtension<TIn, TWorkspace>
PositioningThe Order property (500, 600, 700…)Position relative to a base step, located by name
RegistrationThe DependencyName attributeAutomatic — implementing the interface is enough
How many per chainMany handlers per chainOne extension per pipeline (per TIn/TWorkspace pair)
Continue vs. stopCall NextHandler.Execute to continue, or return to stopSteps run automatically; set workspace.ExitNow = true to stop
Method signatureExecute(IUnitOfWork, TIn, TOut)(TIn parameter, TWorkspace workspace, CancellationToken) returning a Task

Because steps are targeted by name rather than by a numeric order, the model is closer to Spire's extensibility model: you locate the base step you care about and insert, remove, or replace relative to it.

Requirements for a Cart v2 extension

For an extension to participate in a pipeline, it must do the following:

  • Implement the IAsyncPipelineExtension<TIn, TWorkspace> interface, using the same TIn and TWorkspace types as the pipeline you want to extend.
  • Implement the single Extend method, which receives the base code's ordered, named steps and returns the steps to run (modified as needed).
  • Live in your Extensions project.

Unlike a v1 handler, an extension does not need the DependencyName attribute and does not declare an Order. The platform discovers it at startup by the interface it implements and wires it into the matching pipeline. Only one extension is allowed per pipeline; registering two extensions for the same TIn/TWorkspace combination causes a startup error.

The Extend method has this shape:

IEnumerable<Func<TIn, TWorkspace, CancellationToken, Task>> Extend(
    IEnumerable<(string Name, Func<TIn, TWorkspace, CancellationToken, Task> Method)> original
);

The original sequence is the base pipeline's steps, in execution order, each paired with a Name. The names come from the base pipeline's step methods (for example, the GetCart pipeline exposes steps named PopulateUserDataAsync, PopulateSettingsAsync, and CopyCustomPropertiesToResultAsync). You return the steps you want the pipeline to run — the same steps, a filtered set, or a set with your own steps inserted.

Add a step

The following procedure adds a new step to the GetCart pipeline. This is the Cart v2 equivalent of the Cart v1 example that adds the ShowTaxAndShippingForBuyer3 handler to the GetCartHandler chain. The new step selectively shows taxes and shipping based on the current user's role.

📘

Prerequisite

ISC SDK installed

  1. In your Extensions project, create a new extension class.

    public class ShowTaxAndShippingForBuyer3
    {
    }
  2. Implement IAsyncPipelineExtension<TIn, TWorkspace>. For the TIn and TWorkspace types, specify GetCartParameter and the Cart v2 GetCartResult, respectively. This is how an extension is associated with a pipeline, so it is important to specify the correct types.

    GetCartParameter is in the Insite.Cart.Services.Parameters namespace. The Cart v2 GetCartResult is in the Insite.Cart.Services.Async.Results namespace — note that this is the asynchronous result type, not the legacy Insite.Cart.Services.Results.GetCartResult. In Cart v2, this result type also serves as the pipeline's workspace.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Insite.Cart.Services.Async.Results;
    using Insite.Cart.Services.Parameters;
    using Insite.Core.Interfaces.Plugins.Pipelines;
    using Insite.Core.Security;
    
    public class ShowTaxAndShippingForBuyer3
        : IAsyncPipelineExtension<GetCartParameter, GetCartResult>
    {
    }
  3. Write the step method. A step has the signature (TIn parameter, TWorkspace workspace, CancellationToken) and returns a Task. Because Cart v2 still runs synchronously under the hood (see the Cart v2 overview), this step does its work and returns Task.CompletedTask rather than using async/await.

    private Task ShowTaxAndShippingForBuyer3Async(
        GetCartParameter parameter,
        GetCartResult workspace,
        CancellationToken cancellationToken)
    {
        workspace.ShowTaxAndShipping =
            workspace.UserRoles != null
            && workspace.UserRoles
                .Split(',')
                .Select(role => role.Trim())
                .Contains(BuiltInRoles.Buyer3);
    
        return Task.CompletedTask;
    }
  4. Implement Extend to insert your step into the sequence. Iterate the base steps, yield each one, and yield your step immediately after the base step it should follow. Here the step is inserted after PopulateSettingsAsync, which is the step that sets the default value of ShowTaxAndShipping. Running afterward lets your step override that default, and by this point PopulateUserDataAsync has already populated workspace.UserRoles. This positioning is the Cart v2 equivalent of choosing an Order value in Cart v1.

    public IEnumerable<Func<GetCartParameter, GetCartResult, CancellationToken, Task>> Extend(
        IEnumerable<(string Name, Func<GetCartParameter, GetCartResult, CancellationToken, Task> Method)> original)
    {
        foreach (var step in original)
        {
            yield return step.Method;
    
            if (step.Name == "PopulateSettingsAsync")
            {
                yield return this.ShowTaxAndShippingForBuyer3Async;
            }
        }
    }

    For reference, the following is the completed extension class.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Insite.Cart.Services.Async.Results;
    using Insite.Cart.Services.Parameters;
    using Insite.Core.Interfaces.Plugins.Pipelines;
    using Insite.Core.Security;
    
    public class ShowTaxAndShippingForBuyer3
        : IAsyncPipelineExtension<GetCartParameter, GetCartResult>
    {
        public IEnumerable<Func<GetCartParameter, GetCartResult, CancellationToken, Task>> Extend(
            IEnumerable<(string Name, Func<GetCartParameter, GetCartResult, CancellationToken, Task> Method)> original)
        {
            foreach (var step in original)
            {
                yield return step.Method;
    
                if (step.Name == "PopulateSettingsAsync")
                {
                    yield return this.ShowTaxAndShippingForBuyer3Async;
                }
            }
        }
    
        private Task ShowTaxAndShippingForBuyer3Async(
            GetCartParameter parameter,
            GetCartResult workspace,
            CancellationToken cancellationToken)
        {
            workspace.ShowTaxAndShipping =
                workspace.UserRoles != null
                && workspace.UserRoles
                    .Split(',')
                    .Select(role => role.Trim())
                    .Contains(BuiltInRoles.Buyer3);
    
            return Task.CompletedTask;
        }
    }
  5. Build your solution.

The next time the GetCart pipeline runs, your step executes right after PopulateSettingsAsync, and the cart shows or hides tax and shipping based on the user's role.

📘

Note

Each step receives the same CancellationToken the pipeline was invoked with. Pass it along to any asynchronous platform calls your step makes so cancellation propagates correctly.

Remove a step

Because Extend returns the steps that will run, you remove a base step by leaving it out of the returned sequence. Filter the base steps by Name:

public IEnumerable<Func<GetCartParameter, GetCartResult, CancellationToken, Task>> Extend(
    IEnumerable<(string Name, Func<GetCartParameter, GetCartResult, CancellationToken, Task> Method)> original) =>
    original
        .Where(step => step.Name != "GetAlsoPurchasedProductsAsync")
        .Select(step => step.Method);

This is cleaner than the Cart v1 approach, where you cannot truly remove a handler and instead replace it with one that does nothing. In Cart v2, omitting the step from the returned sequence removes it outright.

Replace a step

To replace base behavior while keeping its position, map the matching step's method to your own and leave the others untouched:

public IEnumerable<Func<GetCartParameter, GetCartResult, CancellationToken, Task>> Extend(
    IEnumerable<(string Name, Func<GetCartParameter, GetCartResult, CancellationToken, Task> Method)> original) =>
    original.Select(step =>
        step.Name == "PopulateSettingsAsync"
            ? this.PopulateSettingsAsync
            : step.Method);

Provide your PopulateSettingsAsync method with the same step signature. The replacement runs in the original step's position, so any later steps that depend on it still behave as expected.

Modify the final output

After all steps run, the pipeline converts the workspace into the output object. To adjust that output, implement IAsyncPipelineExtensionFinish<TWorkspace, TOut>. For the GetCart pipeline, both the workspace and the output are GetCartResult. A single class can implement both extension interfaces.

public class CartResultExtension
    : IAsyncPipelineExtension<GetCartParameter, GetCartResult>,
      IAsyncPipelineExtensionFinish<GetCartResult, GetCartResult>
{
    public IEnumerable<Func<GetCartParameter, GetCartResult, CancellationToken, Task>> Extend(
        IEnumerable<(string Name, Func<GetCartParameter, GetCartResult, CancellationToken, Task> Method)> original) =>
        original.Select(step => step.Method);

    public Task<GetCartResult> FinishAsync(
        GetCartResult workspace,
        GetCartResult output,
        CancellationToken cancellationToken)
    {
        // Adjust or replace the output here. The base output is provided so you
        // can build on it rather than recreate it.
        return Task.FromResult(output);
    }
}

Because the current Cart v2 pipelines use the same type for the workspace and the output, the finisher is rarely needed — most customizations are better expressed as a step. It becomes useful if Optimizely introduces pipelines that use a dedicated output type in the future.

Things to keep in mind

  • One extension per pipeline. Only one IAsyncPipelineExtension may target a given TIn/TWorkspace combination. Consolidate all of your changes to a pipeline into a single extension class. Registering two for the same pipeline causes a startup error.
  • Target steps by name, not by number. Step names come from the base pipeline's step methods. See Cart API handler for the cart operations and their steps, or inspect the pipeline in the Insite.Cart.Services.Async namespace.
  • Stop early with ExitNow. Setting workspace.ExitNow = true in a step skips the remaining steps; the output conversion still runs. This replaces the v1 pattern of returning from a handler instead of calling NextHandler.Execute.
  • Name-based targeting survives reordering. Inserting relative to a named step is resilient to base changes in a way that numeric ordering is not, but a step you target by name could still be renamed or removed in a future release. Validate your extensions after upgrading.

Did this page help you?