Skip to content

Implement Custom Pricing Rules

⏱ 25 minutes intermediate
📜Corecommerce

Standard price lists cover most scenarios, but real-world pricing often requires logic that goes beyond static price entries. You may need to calculate prices based on external feeds, apply contract-specific rates, adjust prices by time of day, or combine multiple data sources into a final price. Commerce exposes its pricing through an extensible pipeline that you can customize without replacing the entire pricing system.

This guide covers implementing a custom price service, building dynamic pricing logic, and plugging into the price resolver pipeline.

  1. Understand the price resolution pipeline
  2. Implement a custom IPriceService for dynamic pricing
  3. Build a price resolver filter for conditional adjustments
  4. Register and test custom pricing components

When Commerce resolves a price, it follows a pipeline:

  1. IPriceService returns candidate prices from price lists
  2. IPriceOptimizer selects the best price based on customer context
  3. Promotion engine applies any applicable discounts

You can extend any step. The most common extension points are implementing a custom IPriceService for external price sources and adding filters to modify resolved prices.

Replace or decorate the default price service to pull prices from an external system (ERP, PIM, or pricing engine).

Custom price service with external data
csharp
using Mediachase.Commerce;
using Mediachase.Commerce.Pricing;

public class ExternalPriceService : IPriceService
{
    private readonly IPriceService _defaultService;
    private readonly IExternalPriceApi _externalApi;

    public ExternalPriceService(
        IPriceService defaultService,
        IExternalPriceApi externalApi)
    {
        _defaultService = defaultService;
        _externalApi = externalApi;
    }

    public IPriceValue GetDefaultPrice(
        MarketId marketId, DateTime validOn,
        CatalogKey catalogKey, Currency currency)
    {
        // Try external source first
        var externalPrice = _externalApi
            .GetPrice(catalogKey.CatalogEntryCode,
                marketId.Value, currency.CurrencyCode);

        if (externalPrice.HasValue)
        {
            return new PriceValue
            {
                CatalogKey = catalogKey,
                MarketId = marketId,
                UnitPrice = new Money(
                    externalPrice.Value, currency),
                CustomerPricing =
                    CustomerPricing.AllCustomers,
                ValidFrom = DateTime.UtcNow,
                ValidUntil = null,
                MinQuantity = 0
            };
        }

        // Fall back to built-in prices
        return _defaultService.GetDefaultPrice(
            marketId, validOn, catalogKey, currency);
    }

    public IEnumerable<IPriceValue> GetPrices(
        MarketId marketId, DateTime validOn,
        CatalogKey catalogKey,
        PriceFilter filter)
    {
        return _defaultService.GetPrices(
            marketId, validOn, catalogKey, filter);
    }

    public IEnumerable<IPriceValue> GetCatalogEntryPrices(
        IEnumerable<CatalogKey> catalogKeys)
    {
        return _defaultService
            .GetCatalogEntryPrices(catalogKeys);
    }
}

For conditional adjustments (loyalty discounts, time-based pricing, contract rates), implement a filter that modifies the resolved price.

Dynamic pricing filter
csharp
using Mediachase.Commerce;
using Mediachase.Commerce.Pricing;

public class LoyaltyPriceFilter
{
    private readonly ICustomerGroupService
        _groupService;

    public LoyaltyPriceFilter(
        ICustomerGroupService groupService)
    {
        _groupService = groupService;
    }

    public IPriceValue AdjustPrice(
        IPriceValue originalPrice,
        Guid customerId)
    {
        var loyaltyTier = _groupService
            .GetLoyaltyTier(customerId);

        decimal discount = loyaltyTier switch
        {
            "Gold" => 0.10m,
            "Platinum" => 0.15m,
            "Diamond" => 0.20m,
            _ => 0m
        };

        if (discount == 0m)
            return originalPrice;

        var adjustedAmount = originalPrice
            .UnitPrice.Amount * (1 - discount);

        return new PriceValue
        {
            CatalogKey = originalPrice.CatalogKey,
            MarketId = originalPrice.MarketId,
            UnitPrice = new Money(adjustedAmount,
                originalPrice.UnitPrice.Currency),
            CustomerPricing =
                originalPrice.CustomerPricing,
            ValidFrom = originalPrice.ValidFrom,
            ValidUntil = originalPrice.ValidUntil,
            MinQuantity = originalPrice.MinQuantity
        };
    }
}

Register your custom services using dependency injection so Commerce uses them instead of (or alongside) the defaults.

Service registration
csharp
using Microsoft.Extensions.DependencyInjection;

public static class PricingServiceRegistration
{
    public static IServiceCollection
        AddCustomPricing(
            this IServiceCollection services)
    {
        // Decorate the default IPriceService
        services.Decorate<IPriceService,
            ExternalPriceService>();

        // Register the loyalty filter
        services.AddSingleton<LoyaltyPriceFilter>();

        // Register the external API client
        services.AddHttpClient<IExternalPriceApi,
            ExternalPriceApi>(client =>
        {
            client.BaseAddress = new Uri(
                "https://pricing.example.com/api/");
            client.Timeout = TimeSpan.FromSeconds(5);
        });

        return services;
    }
}

Key design decisions:

ApproachWhen to use
Decorate IPriceServiceExternal price source that replaces or augments stored prices
Price filterConditional adjustments based on customer context
Custom IPriceOptimizerChange how Commerce selects among multiple candidate prices
Promotion engine extensionDiscounts that should appear as promotions in the cart
IssueCauseFix
Custom prices not appearingService not registered or decorator order incorrectVerify DI registration and ensure your decorator wraps the default service
External API timeout slowing page loadsNo caching on external price callsAdd a short-lived cache (30-60 seconds) around external price lookups
Price shows original amount, not adjustedFilter not called in the resolution pipelineEnsure your filter is invoked after the default price resolution
Negative prices after discountDiscount percentage exceeds 100%Add a floor check (Math.Max(0, adjustedAmount)) in your filter