Example - product catalog
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 to store products as reusable content items. The example follows the product catalog modeling recommendations and showcases how to:
- Define a reusable field schema for common product fields
- Create a product content type that implements the schema
- Use taxonomies for product categorization
- Retrieve and display products using the content retriever API
Content model overview
The example catalog uses the following content model structure:
- ProductFields – a reusable field schema containing common product properties (name, description, price, category).
- ProductSKU – a content type that implements the ProductFields schema and represents individual products.
- ProductCategory – a taxonomy for organizing products into categories.
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, you get an interface that content types can implement:
/// <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 productProductFieldsDescription– a detailed product descriptionProductFieldsPrice– the base price of the productProductFieldCategory– taxonomy tags for categorization
Product content type
The ProductSKU content type implements the ICodesamplesProductFields interface, inheriting all fields from the reusable schema. The generated code provides a strongly-typed class for working with product data:
/// <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; }
}
The [RegisterContentTypeMapping] attribute automatically registers the class with the content retriever, 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.
Retrieve all products
/// <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
RetrieveContentParametersto specify the language and preview mode - Supports caching through
RetrievalCacheSettings - Transforms content items to view models using the
configureModeldelegate
Retrieve a single product
/// <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
/// <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:
/// <summary>
/// Service for retrieving product categories from the ProductCategory taxonomy.
/// </summary>
public class CategoryService
{
public const string PRODUCT_CATEGORY_TAXONOMY = "Codesamples.ProductCategory";
private readonly ITaxonomyRetriever taxonomyRetriever;
private readonly IProgressiveCache progressiveCache;
private readonly ICacheDependencyBuilderFactory cacheDependencyBuilderFactory;
public CategoryService(
ITaxonomyRetriever taxonomyRetriever,
IProgressiveCache progressiveCache,
ICacheDependencyBuilderFactory cacheDependencyBuilderFactory)
{
this.taxonomyRetriever = taxonomyRetriever;
this.progressiveCache = progressiveCache;
this.cacheDependencyBuilderFactory = cacheDependencyBuilderFactory;
}
/// <summary>
/// Retrieves all product categories from the ProductCategory taxonomy with caching.
/// </summary>
public async Task<List<CategoryModel>> GetCategoriesAsync()
{
return await progressiveCache.LoadAsync(async (cacheSettings) =>
{
// Load 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
})
.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;
},
// Caches the result for 60 minutes, using sliding expiration
new CacheSettings(
cacheMinutes: CacheConstants.LongCacheDurationMinutes,
useSlidingExpiration: true,
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 to apply catalog promotions:
/// <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 after applying any active catalog discounts.
- Preserves the original list price for display (e.g., for showing strikethrough pricing).
- Extracts category information from taxonomy tags.
For better performance in product listings, consider caching the price calculation results or performing bulk calculations for multiple products at once.