Implement price calculation
Features described on this page require the Xperience by Kentico Advanced license tier.
Developer preview feature
The price calculation service is currently not fully functional, and primarily intended to allow technical users to familiarize themselves with the development process. Expect the feature to be updated and extended in upcoming releases.
The related API is marked as experimental and usage will result in warnings when compiling your project. The warnings are treated as errors for reporting purposes. To use the code, you need to suppress the warnings.
What should you do with this feature?
- DO try out development of price calculation service and calculation steps.
- DO feel free to share your feedback with the Kentico Product team.
- DO NOT use the feature in production projects.
The price calculation service IPriceCalculationService
is responsible for calculating prices, taxes, shipping costs, and totals for shopping carts and orders.
This page describes the price calculation flow the service uses to calculate the order price at various phases of the checkout process, and introduces available customization options.
Getting started
To quickly get a price calculation service usable in your project, you need to implement two essential components:
- Product data retriever – Create a class that implements
IProductDataRetriever<TProductIdentifier, TProductData>
to load product information and pricing data from your content management system. This is required because the service needs access to your product catalog to perform calculations.
- See ProductDataRetriever.cs in the DancingGoat sample project for an example of a possible real-world implementation.
- See Implement product data retrieval for more details.
- Tax calculation – Override the default
ITaxPriceCalculationStep
implementation, which doesn’t perform any operations by default. Without this, your price calculations will not include any tax calculations.
- See DancingGoatTaxPriceCalculationStep.cs in the DancingGoat sample project for an example of a possible real-world implementation.
- See Modify existing calculation steps for more details.
Once you’ve implemented and registered these two components, the price calculation service automatically handles the rest of the calculation pipeline, including unit price calculations, subtotals, shipping costs, and final totals.
To calculate the price of an order, use IPriceCalculationService.Calculate(PriceCalculationRequest)
. Prepare a PriceCalculationRequest
by collecting shopping cart data, selected shipping and payment methods, and customer details from the context of the current order.
Info: This example uses ShoppingCartData
, ShoppingCartDataItem
, and ShoppingCartDataModel
classes from the Dancing Goat sample site to demonstrate a typical implementation.
For a complete implementation example, examine the CalculationService.cs file of the Dancing Goat sample site, which demonstrates the full shopping cart calculation implementation with proper error handling and integration with other services.
// An instance of the price calculation service, obtained via dependency injection
private readonly IPriceCalculationService<PriceCalculationRequest, PriceCalculationResult> priceCalculationService;
// Creates the calculation request with contextual data from the current order
var calculationRequest = new PriceCalculationRequest
{
Items = [.. shoppingCartData.Items.Select(item => new PriceCalculationRequestItem
{
ProductIdentifier = item.ProductIdentifier,
Quantity = item.Quantity,
})],
ShippingMethodId = shippingMethodId,
PaymentMethodId = paymentMethodId,
BillingAddress = customerAddress,
CustomerId = customerId,
LanguageName = "en"
};
// Calculates the prices based on the request
PriceCalculationResult calculationResult = await priceCalculationService.Calculate(calculationRequest, cancellationToken);
// Total price of items from the shopping cart
decimal totalPrice = calculationResult.TotalPrice;
// Total tax amount on items from the shopping cart
decimal totalTax = calculationResult.TotalTax;
// TotalTax + TotalPrice
decimal subTotal = calculationResult.Subtotal;
// The price for shipping
decimal shippingPrice = calculationResult.ShippingPrice;
// Grand total to be paid by the customer, including shipping and taxes and applied discounts
decimal grandTotal = calculationResult.GrandTotal;
// Items with detailed pricing information
ICollection<PriceCalculationResultItem> items = calculationResult.Items;
Price calculation flow
The price calculation service transforms a PriceCalculationRequest
into a complete PriceCalculationResult
through a series of sequential steps:
Input preparation – You provide a
PriceCalculationRequest
created from an order checkout step. The object contains items to be priced (with their product identifiers and quantities), customer details, selected shipping and payment methods, and delivery addresses.Price calculation steps – The
IPriceCalculationService
coordinates the execution of calculation steps. Each calculation step performs a specific pricing task, modifying thePriceCalculationResult
as it progresses through the pipeline. Steps execute sequentially, with each step building upon previous calculations.Result compilation – The service returns a
PriceCalculationResult
containing itemized pricing details, subtotals, shipping costs, taxes, and the grand total.
The following diagram illustrates the high-level data flow:
The price calculation service executes the following default calculation steps in order:
- Load product data – loads product information and pricing data using the registered implementation of the
IProductDataRetriever
interface. This step retrieves product information from your content management system, loading essential pricing data such as unit prices and any additional product attributes needed for subsequent calculations. - Calculate unit price – calculates base unit prices for items by multiplying the product’s unit price by the requested quantity. Sets the
LineTotal
property on each result item. - Calculate total value – performs an initial calculation of order totals. Calculates the
Subtotal
(sum of all line totals) and sets preliminary values forTotalPrice
andGrandTotal
before shipping and taxes are applied. - Calculate shipping – determines the shipping cost based on the selected shipping method. If a shipping method ID is provided in the calculation request, this step retrieves the corresponding shipping method and sets the
ShippingPrice
. - Calculate taxes – calculates applicable taxes for the order. The default implementation is a no-op – you must provide a custom implementation with your specific tax calculation logic. This step should calculate and set the
TotalTax
on the result. - Calculate total value including shipping and taxes – recalculates the final totals to include shipping costs and taxes. Updates
TotalPrice
to include shipping and sets the finalGrandTotal
. Executing this step twice (before and after shipping/tax) maintains accurate totals at each stage.
The following diagram illustrates the complete calculation flow of the default pipeline, from the initial request through each calculation step to the final result:
You can customize the default flow by implementing custom calculation steps or modifying existing ones.
Shopping cart price calculation
When calculating the intermediate price of a shopping cart, the shipping and payment method price is usually not included in the calculation, as these methods are typically selected during the checkout process. To calculate the price of a shopping cart, use the Calculate
method of the IPriceCalculationService
, but the shipping and payment method IDs, as well as customer address, can be set to null
in the price calculation request.
// An instance of the price calculation service, obtained via dependency injection
private readonly IPriceCalculationService<PriceCalculationRequest, PriceCalculationResult> priceCalculationService;
// Shopping cart content retrieved from the current shopping cart
ShoppingCartDataModel shoppingCartData;
// The customer ID of the current user
int customerId;
// Creates the calculation request
var calculationRequest = new PriceCalculationRequest
{
Items = [.. shoppingCartData.Items.Select(item => new PriceCalculationRequestItem
{
ProductIdentifier = item.ProductIdentifier,
Quantity = item.Quantity,
})],
ShippingMethodId = null,
PaymentMethodId = null,
BillingAddress = null,
CustomerId = customerId,
LanguageName = "en"
};
// Calculates the prices based on the request
var calculationResult = await priceCalculationService.Calculate(calculationRequest, cancellationToken);
// Total price of items from the shopping cart
decimal totalPrice = calculationResult.TotalPrice;
// Total tax amount on items from the shopping cart
decimal totalTax = calculationResult.TotalTax;
// TotalTax + TotalPrice
decimal subTotal = calculationResult.Subtotal;
// Items with detailed pricing information
ICollection<PriceCalculationResultItem> items = calculationResult.Items;
For a complete implementation example, examine the CalculationService.cs file of the Dancing Goat sample site, which demonstrates the full shopping cart calculation implementation with proper error handling and integration with other services.
Implement product data retrieval
The price calculation service requires a custom implementation of IProductDataRetriever<TProductIdentifier, TProductData>
to provide product information for pricing calculations. The product data retriever is data store agnostic – you can retrieve product data from any source, including Xperience’s content hub, external databases, third-party APIs, or custom data stores, as long as you return the data in the expected format.
The IProductDataRetriever
interface defines a single method that you must implement:
public interface IProductDataRetriever<TProductIdentifier, TProductData>
{
Task<IDictionary<TProductIdentifier, TProductData>> Get(
IEnumerable<TProductIdentifier> productIdentifiers,
string languageName,
CancellationToken cancellationToken = default
);
}
The Get
method accepts the following parameters:
productIdentifiers
– collection of product identifiers from the calculation request items. The method receives all identifiers at once, allowing efficient bulk retrieval.languageName
– the language for localized product data (fromPriceCalculationRequest.LanguageName
). Use this to retrieve language-specific product information such as names or region-specific pricing.cancellationToken
– pass this token to async operations to support request cancellation.
And must return a dictionary mapping each product identifier to its corresponding product data. The dictionary keys must match the identifiers from the input parameter. If a product cannot be found, you can either omit it from the dictionary or include it with null values, depending on your error handling strategy.
ProductData and ProductIdentifier objects
The product data retriever uses two objects for storing retrieved product information:
ProductIdentifier
A record that identifies a product in your catalog. The default ProductIdentifier
contains a single ProductId
property:
public record ProductIdentifier
{
public int ProductId { get; init; }
}
If you need additional identification fields (such as variant identifiers, SKU codes, or multi-part keys), extend the ProductIdentifier record:
public record ProductWithVariantIdentifier : ProductIdentifier
{
public int? VariantId { get; init; }
}
ProductData
A record containing the product information needed for price calculations. The default ProductData
requires a UnitPrice
property (used by the unit price calculation step):
public record ProductData
{
public decimal UnitPrice { get; init; }
}
You can extend ProductData to include additional fields required by your pricing logic, such as product weight for shipping calculations, tax categories, or discount eligibility:
public record CustomProductData : ProductData
{
public decimal Weight { get; init; }
public string TaxCategory { get; init; }
public string ProductCategory { get; init; }
}
Implementation steps
Follow these steps to implement the product data retriever:
Decide whether to use the default
ProductIdentifier
andProductData
types or extend them with additional fields required by your pricing logic.Implement the
IProductDataRetriever<TProductIdentifier, TProductData>
interface using your chosen types:C#Product data retriever classpublic class ProductDataRetriever : IProductDataRetriever<ProductIdentifier, ProductData> { public async Task<IDictionary<ProductIdentifier, ProductData>> Get( IEnumerable<ProductIdentifier> productIdentifiers, string languageName, CancellationToken cancellationToken) { // Implementation goes here } }
Use the content API to query Xperience’s content hub, or retrieve data from your external data source. For optimal performance, retrieve all products in a single bulk query rather than individual requests:
C#Bulk retrieval example using content retriever APIvar productIds = productIdentifiers.Select(p => p.ProductId).ToList(); var products = await contentRetriever.RetrieveContent<ProductItem>( RetrieveContentParameters.Default, queryParameters => queryParameters .WhereIn(nameof(IContentItemFieldsSource.SystemFields.ContentItemID), productIds), new RetrievalCacheSettings(cacheItemNameSuffix: $"{nameof(ContentTypesQueryParameters.WhereIn)}|ByContentItemIDs"), cancellationToken: cancellationToken);
Transform the retrieved data into
ProductData
objects with the requiredUnitPrice
property and any custom fields:C#Mapping to ProductDatareturn products.ToDictionary( p => new ProductIdentifier { ProductId = p.SystemFields.ContentItemID }, p => new ProductData { UnitPrice = p.GetValue<decimal>("ProductPrice") } );
Register your implementation in the dependency injection container:
C#Program.csbuilder.Services.AddTransient<IProductDataRetriever<ProductIdentifier, ProductData>, ProductDataRetriever>();
Calculation pipeline integration
The ProductDataLoaderCalculationStep
automatically calls your retriever implementation during the calculation process:
- The step extracts product identifiers from all
PriceCalculationRequestItem
objects in the request - It calls your retriever’s
Get
method with these identifiers and the request’s language name - The retrieved
ProductData
is attached to each correspondingPriceCalculationResultItem
- Subsequent calculation steps (unit price, tax, etc.) can access this product data
This automatic integration means you only need to implement the retriever – the price calculation service handles calling it at the appropriate time.
Preparing calculation requests
How you create and populate PriceCalculationRequest
objects depends entirely on your checkout process implementation. The price calculation service does not prescribe a specific approach – you control how data flows from your shopping cart, checkout forms, and user sessions into the calculation request. This section highlights common implementation patterns you can use.
Create a dedicated service layer
Create a dedicated service (such as CalculationService
in the Dancing Goat sample) that handles the complexity of gathering data from multiple sources and constructing the calculation request. This approach centralizes request creation logic and makes it easier to maintain and test.
Dynamic data gathering
Retrieve customer information, shipping selections, and cart contents from various sources at the time of calculation:
- Shopping cart data from session storage or a database
- Customer ID from the authenticated user’s identity
- Shipping and payment method selections from checkout form state
- Billing/shipping addresses from user profiles or form submissions
- Language and currency preferences from the current request context
Incremental calculations
Call the price calculation service at different points in your checkout flow with progressively more data as it becomes available from the customer:
- Initial cart view – Calculate with items only, no shipping or customer details
- Shipping selection – Recalculate with shipping method included
- Final checkout – Calculate with complete data including customer address and payment method
The Dancing Goat sample site demonstrates this approach in its CalculationService
class, which provides separate methods for calculations with and without shipping information. The service gathers customer data from ASP.NET Core Identity, retrieves customer records from the database, and transforms address view models into PriceCalculationRequestAddress
objects.
For a complete working example that shows how to integrate request creation with user authentication, form data, and session state, examine the CalculationService.cs file in the Dancing Goat sample site.
Customization options
You can customize the price calculation service by implementing custom calculation steps or modifying existing ones to fit your business requirements. Additionally, you can extend the core data transfer objects to include additional fields required by your pricing logic.
Extend data transfer objects
To customize a data transfer object, create a new class that inherits from the respective object, add your custom fields, and use the custom class in your calculation steps and services. Note that when you extend these classes, you also need to update any related classes or services that utilize them to ensure compatibility with your custom fields.
The following objects are available for extension:
PriceCalculationRequest
PriceCalculationResult
PriceCalculationRequestItem
PriceCalculationResultItem
ProductIdentifier
ProductData
Modify existing calculation steps
You can modify the behavior of existing calculation steps to implement custom pricing logic without creating entirely new steps. When you register a custom step implementation, it automatically replaces the default step implementation in the calculation pipeline.
The following calculation step interfaces can be overridden:
IProductDataLoaderPriceCalculationStep<TCalculationRequest, TCalculationResult>
– loads product information and pricing data using the given implementation of IProductDataRetriever.IUnitPriceCalculationStep<TCalculationRequest, TCalculationResult>
– calculates base unit prices for items.ITotalValuesPriceCalculationStep<TCalculationRequest, TCalculationResult>
– calculates subtotals and totals.IShippingPriceCalculationStep<TCalculationRequest, TCalculationResult>
– calculates shipping costs.ITaxPriceCalculationStep<TCalculationRequest, TCalculationResult>
– calculates applicable taxes.
To customize an existing calculation step, follow these steps:
Implement a custom class that inherits from the respective interface of an existing calculation step you want to modify. Override the
Execute
method to implement your custom logic while optionally calling the base implementation.- For example, when customizing the
TaxCalculationStep
, you need to create a class that inherits fromITaxPriceCalculationStep<TCalculationRequest, TCalculationResult>
and override theExecute
method to apply your custom tax logic.
C#public sealed class CustomTaxCalculationStep<TCalculationRequest, TCalculationResult> : ITaxPriceCalculationStep<TCalculationRequest, TCalculationResult> where TCalculationRequest : PriceCalculationRequest where TCalculationResult : PriceCalculationResult { public Task Execute(IPriceCalculationData<TCalculationRequest, TCalculationResult> calculationData, CancellationToken cancellationToken) { // Custom tax calculation logic return Task.CompletedTask; } }
- For example, when customizing the
Register your implementation in the dependency injection container to make it available to the price calculation service. When you register a custom step implementation, it automatically replaces the default step implementation in the calculation pipeline.
C#Program.csbuilder.services.AddTransient<ITaxPriceCalculationStep<,>, CustomTaxCalculationStep>();
For a complete implementation example, examine the DancingGoatTaxPriceCalculationStep.cs file in the Dancing Goat sample site, which demonstrates a custom tax calculation step implementation.
Implement custom calculation steps
Custom calculation steps allow you to add specialized pricing logic such as volume-based discounts, loyalty program benefits, handling fees, gift wrapping charges, or complex tax calculations. Custom steps are typically inserted between the initial total calculation (step 3) and shipping calculation (step 4) in the pipeline. To implement a custom calculation step, follow these steps:
Implement a custom class that implements
IPriceCalculationStep<TCalculationRequest, TCalculationResult>
. In theExecute
method, implement your custom logic to modify the calculation result based on the calculation request.C#public sealed class CustomCalculationStep<TRequest, TResult> : IPriceCalculationStep<TCalculationRequest, TCalculationResult> where TRequest : PriceCalculationRequest where TResult : PriceCalculationResult { public Task Execute(IPriceCalculationData<TRequest, TResult> calculationData, CancellationToken cancellationToken) { // Custom calculation logic return Task.CompletedTask; } }
Register your implementation in the dependency injection container to make it available to the price calculation service.
C#Program.csbuilder.services.AddTransient<IPriceCalculationStep<,>, CustomCalculationStep>();
Implement a custom calculation steps provider that implements
IPriceCalculationStepsProvider<TCalculationRequest, TCalculationResult>
and includes your new step in the calculation pipeline. This allows you to control which steps execute and in what order, reorganize the default pipeline, add custom steps, or conditionally include steps based on request properties.- When you register a custom implementation of the
IPriceCalculationStepsProvider
, you need to ensure that all required calculation steps are included in the custom provider, including the default calculation steps. If any required step is omitted, the price calculation service may not function correctly, leading to incomplete or incorrect price calculations. See the top of this page for a complete list of default calculation steps.
C#Custom calculation steps providerpublic class CustomCalculationStepsProvider : IPriceCalculationStepsProvider<PriceCalculationRequest, PriceCalculationResult> { private readonly IProductDataLoaderPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> productDataLoader; private readonly IUnitPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> unitPriceStep; private readonly ITotalValuesPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> totalValuesStep; private readonly IShippingPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> shippingStep; private readonly ITaxPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> taxStep; private readonly IPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> customDiscountStep; public CustomCalculationStepsProvider( IProductDataLoaderPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> productDataLoader, IUnitPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> unitPriceStep, ITotalValuesPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> totalValuesStep, IShippingPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> shippingStep, ITaxPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> taxStep, IPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult> customDiscountStep) { this.productDataLoader = productDataLoader; this.unitPriceStep = unitPriceStep; this.totalValuesStep = totalValuesStep; this.shippingStep = shippingStep; this.taxStep = taxStep; this.customDiscountStep = customDiscountStep; } public IEnumerable<IPriceCalculationStep<PriceCalculationRequest, PriceCalculationResult>> Get() { // Step 1: Load product data (default) yield return productDataLoader; // Step 2: Calculate unit prices (default) yield return unitPriceStep; // Step 3: Calculate initial totals (default) yield return totalValuesStep; // Step 4: Apply custom discount logic (newly added custom step) yield return customDiscountStep; // Step 5: Calculate shipping costs (default) yield return shippingStep; // Step 6: Calculate taxes (default) yield return taxStep; // Step 7: Recalculate final totals (default) yield return totalValuesStep; } }
- When you register a custom implementation of the
Register the custom steps provider to replace the default calculation pipeline. When you register your custom steps provider, the calculation service automatically detects and uses your custom steps provider, executing your custom step during calculations.
C#Program.csbuilder.services.AddTransient<IPriceCalculationStepsProvider<PriceCalculationRequest, PriceCalculationResult>, CustomCalculationStepsProvider>();