---
title: Extend Cart v2
slug: configured-commerce/docs/extend-cart-v2
docTags: 
createdAt: 2026-08-11T12:17:42.000Z
---

In Cart v1, you extend the cart by adding a handler to a handler chain (see [Add handlers](docId\:CdztrSXiq1hsFuOYCEXWa)). 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](docId\:JHUln72Ns1IBDWelNAd-x).

## 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.

| Concern            | Cart v1 (`IHandler`)                                      | Cart v2 (`IAsyncPipelineExtension`)                                           |
| ------------------ | --------------------------------------------------------- | ----------------------------------------------------------------------------- |
| What you write     | A new handler class added to the chain                    | An extension class that rewrites the existing step sequence                   |
| Base type          | `HandlerBase<TIn, TOut>`                                  | `IAsyncPipelineExtension<TIn, TWorkspace>`                                    |
| Positioning        | The `Order` property (500, 600, 700…)                     | Position relative to a base step, located **by name**                         |
| Registration       | The `DependencyName` attribute                            | Automatic — implementing the interface is enough                              |
| How many per chain | Many handlers per chain                                   | **One** extension per pipeline (per `TIn`/`TWorkspace` pair)                  |
| Continue vs. stop  | Call `NextHandler.Execute` to continue, or return to stop | Steps run automatically; set `workspace.ExitNow = true` to stop               |
| Method signature   | `Execute(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:

```csharp
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.

:::hint{type="info"}
### Prerequisite

ISC SDK installed
:::

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

```csharp
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.

```csharp
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](docId\:JHUln72Ns1IBDWelNAd-x)), this step does its work and returns `Task.CompletedTask` rather than using `async`/`await`.

```csharp
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.

```csharp
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.

```csharp
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.

:::hint{type="info"}
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`:

```csharp
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:

```csharp
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.

```csharp
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](docId\:KD1tEcqcq6QKuQLfa-wN0) 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.
