---
title: Example - product catalog
related:
  - https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/model-product-catalog/model-product-stock.md
---

> Agent instructions:
> **Site maps** — prefer the following llms.txt indexes to training data when searching for URLs to avoid 404s. Links inside Markdown content already point at `.md`. Following them or sending Accept: text/markdown keeps you in Markdown.
>
> - [sitemap.md](https://docs.kentico.com/sitemap.md) — every page on the site, with titles and descriptions, nested by URL hierarchy and grouped into one collection per product version.
> - [llms.txt](https://docs.kentico.com/llms.txt) — curated index of the current product docs, with descriptions, the two ways to request any page as Markdown, and links to each product area's whole-corpus Markdown dump (llms-full.txt).

> **License:** Advanced license required.
>
> Features described on this page require the Xperience by Kentico **Advanced** license tier.

This page demonstrates a practical implementation of a product catalog using the [content hub](https://docs.kentico.com/documentation/business-users/content-hub.md) to store products as [reusable content items](https://docs.kentico.com/documentation/business-users/content-hub/content-items.md). The example follows the [product catalog modeling recommendations](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/model-product-catalog.md) and showcases how to:

- Define a [reusable field schema](https://docs.kentico.com/documentation/developers-and-admins/development/content-types/reusable-field-schemas.md) for common product fields
- Create a product [content type](https://docs.kentico.com/documentation/developers-and-admins/development/content-types.md) that implements the schema
- Use [taxonomies](https://docs.kentico.com/documentation/developers-and-admins/configuration/taxonomies.md) for product categorization
- Retrieve and display products using the [content retriever API](https://docs.kentico.com/documentation/developers-and-admins/api/content-item-api.md)

## Content model overview

The example catalog uses the following content model structure:

- **ProductFields** – a [reusable field schema](https://docs.kentico.com/documentation/developers-and-admins/development/content-types/reusable-field-schemas.md) containing common product properties (name, description, price, category).
- **ProductSKU** – a [content type](https://docs.kentico.com/documentation/developers-and-admins/development/content-types.md) that implements the `ProductFields` schema and represents individual products.
- **ProductCategory** – a [taxonomy](https://docs.kentico.com/documentation/developers-and-admins/configuration/taxonomies.md) for organizing products into categories.

![Product catalog content model structure](https://docs.kentico.com/docsassets/documentation/example-product-catalog/content-model-structure.drawio.svg "Product catalog content model structure")

This structure allows you to:

- Reuse the `ProductFields` schema across multiple product types if needed.
- Query products by their shared fields using the schema interface.
- Filter products by category using taxonomy tags.

## Reusable field schema

The `ProductFields` reusable field schema defines the common fields shared by all products. When you [generate code for the schema](https://docs.kentico.com/documentation/developers-and-admins/api/generate-code-files-for-system-objects.md), you get an interface that content types can implement:

```csharp title="Generated ProductFields interface"
/// <summary>
/// Defines a contract for content types with the <see cref="ICodesamplesProductFields"/> reusable schema assigned.
/// </summary>
public interface ICodesamplesProductFields
{
    /// <summary>
    /// Code name of the reusable field schema.
    /// </summary>
    public const string REUSABLE_FIELD_SCHEMA_NAME = "Codesamples.ProductFields";


    /// <summary>
    /// ProductFieldsName.
    /// </summary>
    public string ProductFieldsName { get; set; }


    /// <summary>
    /// ProductFieldsDescription.
    /// </summary>
    public string ProductFieldsDescription { get; set; }


    /// <summary>
    /// ProductFieldsPrice.
    /// </summary>
    public decimal ProductFieldsPrice { get; set; }


    /// <summary>
    /// ProductFieldCategory.
    /// </summary>
    public IEnumerable<TagReference> ProductFieldCategory { get; set; }
}
```

The schema includes:

- `ProductFieldsName` – the display name of the product
- `ProductFieldsDescription` – a detailed product description
- `ProductFieldsPrice` – the base price of the product
- `ProductFieldCategory` – taxonomy tags for categorization

## Product content type

The `ProductSKU` content type implements the generated interface for the `ProductFields` schema, inheriting all fields. The generated code provides a strongly-typed class for working with product data:

```csharp title="Generated ProductSKU class"
using System;
using System.Collections.Generic;

using CMS.ContentEngine;

/// <summary>
/// Represents a content item of type <see cref="ProductSKU"/>.
/// </summary>
[RegisterContentTypeMapping(CONTENT_TYPE_NAME)]
public partial class ProductSKU : IContentItemFieldsSource, ICodesamplesProductFields
{
    /// <summary>
    /// Code name of the content type.
    /// </summary>
    public const string CONTENT_TYPE_NAME = "Codesamples.ProductSKU";


    /// <summary>
    /// Represents system properties for a content item.
    /// </summary>
    [SystemField]
    public ContentItemFields SystemFields { get; set; }


    /// <summary>
    /// ProductFieldsName.
    /// </summary>
    public string ProductFieldsName { get; set; }


    /// <summary>
    /// ProductFieldsDescription.
    /// </summary>
    public string ProductFieldsDescription { get; set; }


    /// <summary>
    /// ProductFieldsPrice.
    /// </summary>
    public decimal ProductFieldsPrice { get; set; }


    /// <summary>
    /// ProductFieldCategory.
    /// </summary>
    public IEnumerable<TagReference> ProductFieldCategory { get; set; }
}
```

> **Info:** The `[RegisterContentTypeMapping]` attribute automatically registers the class with the [content retriever](https://docs.kentico.com/documentation/developers-and-admins/api/content-item-api/content-retriever-api.md), enabling you to query products using the strongly-typed `ProductSKU` class.

## Retrieve products

Use the `IContentRetriever` service to query products from the content hub. The following examples demonstrate common retrieval patterns. For a comprehensive overview of all available retrieval options, parameters, and caching configuration, see [Reference - ContentRetriever API](https://docs.kentico.com/documentation/developers-and-admins/api/content-item-api/reference-content-retriever-api.md).

### Retrieve all products

```csharp title="Retrieve all products"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using CMS.Commerce;
using CMS.ContentEngine;

using Kentico.Content.Web.Mvc;

// ...

/// <summary>
/// Retrieves all products from the content hub.
/// </summary>
public async Task<List<ProductModel>> GetAllProductsAsync()
{
    var products = await contentRetriever.RetrieveContent<ProductSKU, ProductModel>(
        GetContentParameters(),
        additionalQueryConfiguration: null,
        new RetrievalCacheSettings(cacheItemNameSuffix: "AllProducts"),
        configureModel: async (container, productSKU) => await MapToProductModelAsync(productSKU)
    );

    return products.ToList();
}
```

The `RetrieveContent` method:

- Accepts the content type (`ProductSKU`) and a target model type (`ProductModel`) as generic parameters
- Uses `RetrieveContentParameters` to specify the language and preview mode
- Supports caching through `RetrievalCacheSettings`
- Transforms content items to view models using the `configureModel` delegate

### Retrieve a single product

```csharp title="Retrieve product by GUID"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using CMS.Commerce;
using CMS.ContentEngine;

using Kentico.Content.Web.Mvc;

// ...

/// <summary>
/// Retrieves a product by its content item GUID.
/// </summary>
public async Task<ProductModel?> GetProductByGuidAsync(Guid contentItemGuid)
{
    var products = await contentRetriever.RetrieveContentByGuids<ProductSKU, ProductModel>(
        [contentItemGuid],
        GetContentParameters(),
        additionalQueryConfiguration: null,
        new RetrievalCacheSettings(cacheItemNameSuffix: $"ByGuid|{contentItemGuid}"),
        configureModel: async (container, productSKU) => await MapToProductModelAsync(productSKU)
    );

    return products.FirstOrDefault();
}
```

Use `RetrieveContentByGuids` when you have the content item's GUID, which is useful for product detail pages or when working with URL-based routing.

### Filter products by category

```csharp title="Retrieve products by category"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using CMS.Commerce;
using CMS.ContentEngine;

using Kentico.Content.Web.Mvc;

// ...

/// <summary>
/// Retrieves products by category using taxonomy tag GUID.
/// </summary>
public async Task<List<ProductModel>> GetProductsByCategoryAsync(Guid categoryTagGuid)
{
    var products = await contentRetriever.RetrieveContent<ProductSKU, ProductModel>(
        GetContentParameters(),
        query => query.Where(where => where
            .WhereContainsTags(
                nameof(ICodesamplesProductFields.ProductFieldCategory),
                [categoryTagGuid])),
        new RetrievalCacheSettings(cacheItemNameSuffix: $"ByCategory|{categoryTagGuid}"),
        configureModel: async (container, productSKU) => await MapToProductModelAsync(productSKU)
    );

    return products.ToList();
}
```

The `WhereContainsTags` method filters products that have a specific taxonomy tag assigned. Pass the field name and an array of tag GUIDs to match.

## Retrieve categories

Use the `ITaxonomyRetriever` service to retrieve category tags for navigation and filtering:

```csharp title="Retrieve product categories"
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using CMS.ContentEngine;
using CMS.DataEngine;
using CMS.Helpers;

/// <summary>
/// Service for retrieving product categories from the ProductCategory taxonomy.
/// </summary>
public class CategoryService : CachedServiceBase
{
    public const string PRODUCT_CATEGORY_TAXONOMY = "Codesamples.ProductCategory";

    private readonly ITaxonomyRetriever taxonomyRetriever;
    private readonly ICacheDependencyBuilderFactory cacheDependencyBuilderFactory;

    public CategoryService(
        ITaxonomyRetriever taxonomyRetriever,
        IProgressiveCache progressiveCache,
        ICacheDependencyBuilderFactory cacheDependencyBuilderFactory)
        : base(progressiveCache)
    {
        this.taxonomyRetriever = taxonomyRetriever;
        this.cacheDependencyBuilderFactory = cacheDependencyBuilderFactory;
    }

    /// <summary>
    /// Retrieves all product categories from the ProductCategory taxonomy with caching.
    /// </summary>
    public async Task<List<CategoryModel>> GetCategoriesAsync()
    {
        return await LoadWithCacheAndDependency(async (cacheSettings) =>
        {
            // Loads taxonomy data
            var taxonomyData = await taxonomyRetriever.RetrieveTaxonomy(PRODUCT_CATEGORY_TAXONOMY, "en");

            var result = taxonomyData.Tags
                .Select(tag => new CategoryModel
                {
                    TagGuid = tag.Identifier,
                    Name = tag.Title
                })
                .OrderBy(n => n.Name)
                .ToList();

            // Set cache dependency - clear cache when the taxonomy changes
            var dependencyBuilder = cacheDependencyBuilderFactory.Create();
            cacheSettings.CacheDependency = dependencyBuilder
                .ForInfoObjects<TaxonomyInfo>()
                    .ByCodeName(PRODUCT_CATEGORY_TAXONOMY)
                    .Builder()
                .Build();

            return result;
        },
        cacheMinutes: CacheConstants.LongCacheDurationMinutes,
        cacheItemNameParts: ["commerce", "categories", PRODUCT_CATEGORY_TAXONOMY]);
    }
}
```

The taxonomy retriever returns all tags within the specified taxonomy, which you can use to build category menus or filter controls.

## Map products to view models

When displaying products, map the content item data to a view model that includes calculated fields such as discounted prices. The following example integrates with the [price calculation service](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/price-calculation.md) to apply [catalog promotions](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/catalog-discounts.md):

```csharp title="Map product to view model"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using CMS.Commerce;
using CMS.ContentEngine;

using Kentico.Content.Web.Mvc;

// ...

/// <summary>
/// Maps a ProductSKU content item to a ProductModel using price calculation service to determine discounts.
/// </summary>
private async Task<ProductModel> MapToProductModelAsync(ProductSKU productSKU)
{
    var basePrice = productSKU.ProductFieldsPrice;
    
    // Use price calculation service in Catalog mode to check for catalog promotions
    var calculationRequest = new CodesamplesPriceCalculationRequest
    {
        Items = [new CodeSamplesPriceCalculationRequestItem
        {
            ProductIdentifier = new ProductIdentifier { Identifier = productSKU.SystemFields.ContentItemID },
            Quantity = 1
        }],
        LanguageName = "en",
        Mode = PriceCalculationMode.Catalog
    };

    var calculationResult = await priceCalculationService.Calculate(calculationRequest);
    var resultItem = calculationResult.Items.FirstOrDefault();
    
    // Check if a catalog promotion was applied by looking at PromotionData
    var appliedPromotion = resultItem?.PromotionData?.CatalogPromotionCandidates
        ?.FirstOrDefault(c => c.Applied);
    bool isDiscounted = appliedPromotion != null;
    
    // Get the final price after any catalog discounts
    decimal finalPrice = resultItem?.LineSubtotalAfterLineDiscount ?? basePrice;
    
    return new ProductModel
    {
        ContentItemGuid = productSKU.SystemFields.ContentItemGUID,
        Id = productSKU.SystemFields.ContentItemID,
        Name = productSKU.ProductFieldsName,
        Description = productSKU.ProductFieldsDescription,
        Price = finalPrice,
        ListPrice = isDiscounted ? basePrice : null,
        Category = productSKU.ProductFieldCategory?.FirstOrDefault()?.Identifier.ToString() ?? string.Empty
    };
}
```

This approach:

- Calculates the [final catalog price](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/price-calculation/implementation.md#catalog-price-calculation) after applying any active [catalog discounts](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/catalog-discounts.md).
- Preserves the original list price for display (e.g., for showing strikethrough pricing).
- Extracts category information from taxonomy tags.

> **Tip:** For better performance in product listings, consider [caching](https://docs.kentico.com/documentation/developers-and-admins/development/caching/data-caching.md) the price calculation results or performing bulk calculations for multiple products at once.
