Skip to content

Implement a Custom Pricing Service

⏱ 60 minutes advanced
📜Corecommerce

By the end of this tutorial, you will have a custom pricing service that:

  • Intercepts price resolution at runtime
  • Applies volume-based discount tiers
  • Calculates customer-specific pricing from negotiated contracts
  • Enforces minimum margin floors so discounts never go below cost
  • Supports time-bounded promotional overrides

The built-in pricing engine in Commerce handles static price lists well. When your pricing logic requires runtime calculations — contract terms, dynamic market adjustments, or multi-factor discount stacking — you need a custom pricing service.

Ensure you have a Commerce project with products and at least one price list configured. You should be comfortable with .NET dependency injection and the Commerce pricing interfaces.

Commerce resolves prices through a pipeline. When a page or cart requests a price, the system:

  1. Identifies the variant by catalog key
  2. Looks up applicable price entries (market, currency, customer group, quantity)
  3. Selects the best matching price
  4. Applies any promotions or discount rules
  5. Returns the final price

Your custom service replaces or extends step 2 and 3. You intercept the price lookup and inject your own logic before the system selects the final price.

Step 2: Create the pricing service interface

Section titled “Step 2: Create the pricing service interface”

Define a clean interface for your pricing rules engine. This separates the rule evaluation from the Commerce integration.

IPricingRulesEngine.cs
csharp
public interface IPricingRulesEngine
{
    PriceResult CalculatePrice(
        PriceContext context);
}

public class PriceContext
{
    public string VariantCode { get; set; }
    public string MarketId { get; set; }
    public string CurrencyCode { get; set; }
    public decimal Quantity { get; set; }
    public string CustomerGroup { get; set; }
    public string CustomerId { get; set; }
    public DateTime EvaluationDate { get; set; }
}

public class PriceResult
{
    public decimal UnitPrice { get; set; }
    public decimal OriginalPrice { get; set; }
    public string AppliedRule { get; set; }
    public decimal DiscountPercentage { get; set; }
}

Create the first pricing rule: tiered discounts based on order quantity.

VolumeDiscountRule.cs
csharp
public class VolumeDiscountRule : IPricingRule
{
    private static readonly List<VolumeTier> _tiers = new()
    {
        new(100, 0.05m),   // 5% off at 100+ units
        new(500, 0.10m),   // 10% off at 500+
        new(1000, 0.15m),  // 15% off at 1000+
        new(5000, 0.22m),  // 22% off at 5000+
    };

    public PriceAdjustment Evaluate(
        PriceContext context, decimal basePrice)
    {
        var tier = _tiers
            .Where(t => context.Quantity >= t.MinQuantity)
            .OrderByDescending(t => t.MinQuantity)
            .FirstOrDefault();

        if (tier == null) return PriceAdjustment.None;

        return new PriceAdjustment
        {
            DiscountPercent = tier.DiscountPercent,
            RuleName = $"Volume: {tier.MinQuantity}+ units"
        };
    }
}

public record VolumeTier(
    int MinQuantity, decimal DiscountPercent);

Step 4: Implement customer contract pricing

Section titled “Step 4: Implement customer contract pricing”

Add a rule that looks up negotiated contract prices per customer.

ContractPricingRule.cs
csharp
public class ContractPricingRule : IPricingRule
{
    private readonly IContractRepository _contracts;

    public ContractPricingRule(
        IContractRepository contracts)
    {
        _contracts = contracts;
    }

    public PriceAdjustment Evaluate(
        PriceContext context, decimal basePrice)
    {
        var contract = _contracts.GetActiveContract(
            context.CustomerId,
            context.VariantCode,
            context.EvaluationDate);

        if (contract == null)
            return PriceAdjustment.None;

        var contractPrice = contract.NegotiatedPrice;
        var discount = (basePrice - contractPrice) / basePrice;

        return new PriceAdjustment
        {
            FixedPrice = contractPrice,
            DiscountPercent = discount,
            RuleName = $"Contract: {contract.ContractId}"
        };
    }
}

Step 5: Add time-based promotional pricing

Section titled “Step 5: Add time-based promotional pricing”

Create a rule for time-bounded promotions that automatically activate and expire.

PromotionalPricingRule.cs
csharp
public class PromotionalPricingRule : IPricingRule
{
    private readonly IPromotionRepository _promos;

    public PromotionalPricingRule(
        IPromotionRepository promos)
    {
        _promos = promos;
    }

    public PriceAdjustment Evaluate(
        PriceContext context, decimal basePrice)
    {
        var promo = _promos.GetActivePromotion(
            context.VariantCode,
            context.MarketId,
            context.EvaluationDate);

        if (promo == null)
            return PriceAdjustment.None;

        var promoPrice = promo.Type switch
        {
            PromoType.PercentOff => basePrice *
                (1 - promo.Value / 100m),
            PromoType.FixedPrice => promo.Value,
            PromoType.AmountOff => basePrice - promo.Value,
            _ => basePrice
        };

        return new PriceAdjustment
        {
            FixedPrice = promoPrice,
            RuleName = $"Promo: {promo.Name}"
        };
    }
}

Add a safety rule that prevents any discount from pushing the price below your cost plus a minimum margin.

MarginFloorRule.cs
csharp
public class MarginFloorRule : IPricingRule
{
    private readonly ICostService _costService;
    private const decimal MinMarginPercent = 0.15m;

    public MarginFloorRule(ICostService costService)
    {
        _costService = costService;
    }

    public PriceAdjustment Evaluate(
        PriceContext context, decimal calculatedPrice)
    {
        var cost = _costService
            .GetCost(context.VariantCode);

        if (cost == null)
            return PriceAdjustment.None;

        var floorPrice = cost.Value /
            (1 - MinMarginPercent);

        if (calculatedPrice >= floorPrice)
            return PriceAdjustment.None;

        return new PriceAdjustment
        {
            FixedPrice = floorPrice,
            RuleName = "Margin floor enforced"
        };
    }
}

Combine all rules into a pipeline that evaluates them in priority order and selects the best price for the customer.

PricingRulesEngine.cs
csharp
public class PricingRulesEngine : IPricingRulesEngine
{
    private readonly IEnumerable<IPricingRule> _rules;
    private readonly IPriceDetailService _basePrices;

    public PricingRulesEngine(
        IEnumerable<IPricingRule> rules,
        IPriceDetailService basePrices)
    {
        _rules = rules;
        _basePrices = basePrices;
    }

    public PriceResult CalculatePrice(
        PriceContext context)
    {
        var basePrice = _basePrices
            .GetBasePrice(context.VariantCode,
                context.MarketId, context.CurrencyCode);

        if (basePrice == 0)
            return null;

        var bestPrice = basePrice;
        var appliedRule = "List price";

        foreach (var rule in _rules)
        {
            var adjustment = rule.Evaluate(
                context, basePrice);

            if (adjustment == PriceAdjustment.None)
                continue;

            var candidatePrice = adjustment.FixedPrice
                ?? basePrice * (1 - adjustment.DiscountPercent);

            if (candidatePrice < bestPrice)
            {
                bestPrice = candidatePrice;
                appliedRule = adjustment.RuleName;
            }
        }

        return new PriceResult
        {
            UnitPrice = Math.Round(bestPrice, 2),
            OriginalPrice = basePrice,
            AppliedRule = appliedRule,
            DiscountPercentage = (basePrice - bestPrice)
                / basePrice * 100
        };
    }
}

Register your custom pricing service with the dependency injection container so Commerce uses it instead of the default price resolver.

Startup registration
csharp
public void ConfigureServices(IServiceCollection services)
{
    // Register individual pricing rules
    services.AddScoped<IPricingRule, ContractPricingRule>();
    services.AddScoped<IPricingRule, VolumeDiscountRule>();
    services.AddScoped<IPricingRule, PromotionalPricingRule>();
    services.AddScoped<IPricingRule, MarginFloorRule>();

    // Register the engine
    services.AddScoped<IPricingRulesEngine,
        PricingRulesEngine>();

    // Override the default Commerce price resolver
    services.Intercept<IPriceService>(
        (locator, defaultService) =>
            new CustomPriceService(
                defaultService,
                locator.GetInstance<IPricingRulesEngine>()));
}

Build a diagnostic endpoint that shows which rules were evaluated and which one won. This is essential for debugging pricing issues in production.

Pricing diagnostics API
csharp
[ApiController]
[Route("api/pricing")]
public class PricingDiagnosticsController
    : ControllerBase
{
    private readonly IPricingRulesEngine _engine;

    public PricingDiagnosticsController(
        IPricingRulesEngine engine)
    {
        _engine = engine;
    }

    [HttpGet("diagnose/{variantCode}")]
    [Authorize(Roles = "CommerceAdmins")]
    public IActionResult Diagnose(
        string variantCode,
        [FromQuery] decimal quantity = 1,
        [FromQuery] string customerId = null)
    {
        var context = new PriceContext
        {
            VariantCode = variantCode,
            Quantity = quantity,
            CustomerId = customerId,
            EvaluationDate = DateTime.UtcNow,
            MarketId = "US",
            CurrencyCode = "USD"
        };

        var result = _engine.CalculatePrice(context);

        return Ok(new
        {
            result.UnitPrice,
            result.OriginalPrice,
            result.AppliedRule,
            result.DiscountPercentage,
            EvaluatedAt = context.EvaluationDate
        });
    }
}

Verify that each rule works independently and that the rules interact correctly when multiple apply.

ScenarioInputExpected result
Base price onlySKU “HB-01”, qty 1, no customerList price returned
Volume discountSKU “HB-01”, qty 50010% discount applied
Contract pricingSKU “HB-01”, Acme customerContract price wins
Contract + volumeSKU “HB-01”, qty 1000, AcmeLower of contract or volume price
Promotional overrideSKU “HB-01” during sale periodPromo price applied
Margin floor enforcementAny scenario where discount exceeds cost+15%Floor price enforced
Diagnostics endpointGET /api/pricing/diagnose/HB-01?quantity=500Shows applied rule and calculation details

Run the diagnostics endpoint for each scenario and confirm the AppliedRule field matches expectations. Pay special attention to the margin floor — it should override any discount that would push the price below cost plus your minimum margin.