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.
| 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 sameTInandTWorkspacetypes as the pipeline you want to extend. - Implement the single
Extendmethod, 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.
PrerequisiteISC SDK installed
-
In your Extensions project, create a new extension class.
public class ShowTaxAndShippingForBuyer3 { } -
Implement
IAsyncPipelineExtension<TIn, TWorkspace>. For theTInandTWorkspacetypes, specifyGetCartParameterand the Cart v2GetCartResult, respectively. This is how an extension is associated with a pipeline, so it is important to specify the correct types.GetCartParameteris in theInsite.Cart.Services.Parametersnamespace. The Cart v2GetCartResultis in theInsite.Cart.Services.Async.Resultsnamespace — note that this is the asynchronous result type, not the legacyInsite.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> { } -
Write the step method. A step has the signature
(TIn parameter, TWorkspace workspace, CancellationToken)and returns aTask. Because Cart v2 still runs synchronously under the hood (see the Cart v2 overview), this step does its work and returnsTask.CompletedTaskrather than usingasync/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; } -
Implement
Extendto 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 afterPopulateSettingsAsync, which is the step that sets the default value ofShowTaxAndShipping. Running afterward lets your step override that default, and by this pointPopulateUserDataAsynchas already populatedworkspace.UserRoles. This positioning is the Cart v2 equivalent of choosing anOrdervalue 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; } } -
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.
NoteEach step receives the same
CancellationTokenthe 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
IAsyncPipelineExtensionmay target a givenTIn/TWorkspacecombination. 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.Asyncnamespace. - Stop early with
ExitNow. Settingworkspace.ExitNow = truein a step skips the remaining steps; the output conversion still runs. This replaces the v1 pattern of returning from a handler instead of callingNextHandler.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.
Updated about 22 hours ago
