---
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.
