Custom Episerver Commerce Product/Variant router
Mattias Olsson
12/22/2017 2:26:46 PM
The router works like this:
- When the virtual path for a variant is created, it tries to find the first parent product:
- If a product is found, the router will start by fetching the virtual path for the product.
- If the product only has 1 variant, the variant will get the same virtual path as the product.
- If the product has multiple variants, the current variant route segment is added to the end of the product's virtual path.
- When a product is routed, the next URL segment is matched against the product's variants, and if a match is found it will route to that variant instead. If no next URL segment exists it will route to the first variant (if present).
The code
First of all I have a couple of extension methods to make the code a bit neater. The first is for fetching the first parent product content for a variant and the second is for fetching all variants for a product.
public static T GetFirstParentProductContent<T>(
this EntryContentBase variant,
IContentLoader contentLoader = null,
IRelationRepository relationRepository = null) where T: ProductContent
{
contentLoader = contentLoader ?? ServiceLocator.Current.GetInstance<IContentLoader>();
relationRepository = relationRepository ??
ServiceLocator.Current.GetInstance<IRelationRepository>();
var parentProductLink = variant.GetParentProducts(relationRepository).FirstOrDefault();
T product;
if (contentLoader.TryGet(parentProductLink, out product))
{
return product;
}
return null;
}
public static IEnumerable<TVariant> GetVariantsContent<TVariant>(
this ProductContent product,
IContentLoader contentLoader = null,
IRelationRepository relationRepository = null) where TVariant : VariationContent
{
var locator = ServiceLocator.Current;
contentLoader = contentLoader ?? locator.GetInstance<IContentLoader>();
relationRepository = relationRepository ?? locator.GetInstance<IRelationRepository>();
var variantLinks = product.GetVariants(relationRepository);
return contentLoader.GetItems(variantLinks, product.Language).OfType<TVariant>();
}
Partial router code
public class CustomHierarchicalCatalogPartialRouter : HierarchicalCatalogPartialRouter
{
private readonly IContentLoader _contentLoader;
private readonly IRelationRepository _relationRepository;
public CustomHierarchicalCatalogPartialRouter(
Func<ContentReference> routeStartingPoint,
CatalogContentBase commerceRoot,
bool enableOutgoingSeoUri,
IContentLoader contentLoader,
IRelationRepository relationRepository) : base(
routeStartingPoint,
commerceRoot,
enableOutgoingSeoUri)
{
_contentLoader = contentLoader;
_relationRepository = relationRepository;
}
public override PartialRouteData GetPartialVirtualPath(
CatalogContentBase content,
string language,
RouteValueDictionary routeValues,
RequestContext requestContext)
{
var routeData = base.GetPartialVirtualPath(content, language, routeValues, requestContext);
if (routeData == null)
{
return null;
}
var variant = content as VariationContent;
if (variant != null)
{
return GetVariantPartialVirtualPath(
variant,
routeData,
language,
routeValues,
requestContext);
}
return routeData;
}
protected virtual PartialRouteData GetVariantPartialVirtualPath(
VariationContent variant,
PartialRouteData routeData,
string language,
RouteValueDictionary routeValues,
RequestContext requestContext
)
{
var product = variant.GetFirstParentProductContent<ProductContent>(
this._contentLoader,
this._relationRepository);
if (product != null)
{
var productRouteData = base.GetPartialVirtualPath(
product,
language,
routeValues,
requestContext);
if (productRouteData != null)
{
// If product has more than 1 variant, append variant route segment to URL.
if (product.GetVariants(this._relationRepository).Count() > 1)
{
routeData.PartialVirtualPath =
$"{productRouteData.PartialVirtualPath}/{variant.RouteSegment}/";
}
else
{
routeData.PartialVirtualPath = productRouteData.PartialVirtualPath;
}
}
}
return routeData;
}
public override object RoutePartial(PageData content, SegmentContext segmentContext)
{
var routed = base.RoutePartial(content, segmentContext);
var product = routed as ProductContent;
if (product == null)
{
return routed;
}
SegmentPair segment = segmentContext.GetNextValue(segmentContext.RemainingPath);
IEnumerable<VariationContent> variants = product.GetVariantsContent<VariationContent>(
this._contentLoader,
this._relationRepository);
var variant = !string.IsNullOrWhiteSpace(segment.Next)
? variants?.FirstOrDefault(v => v.RouteSegment.Equals(segment.Next))
: variants?.FirstOrDefault();
// If a variant is found for the product, route to that variant instead.
if (variant != null)
{
segmentContext.RemainingPath = segment.Remaining;
segmentContext.SetCustomRouteData(ProductRoutingConstants.CurrentProductKey, routed);
segmentContext.RoutedContentLink = variant.ContentLink;
segmentContext.RoutedObject = variant;
return variant;
}
return routed;
}
}
If you don't want to route to the first variant for a product, you can easily modify the RoutePartial method so it looks like this:
public override object RoutePartial(PageData content, SegmentContext segmentContext)
{
var routed = base.RoutePartial(content, segmentContext);
var product = routed as ProductContent;
if (product == null)
{
return routed;
}
SegmentPair segment = segmentContext.GetNextValue(segmentContext.RemainingPath);
if (!string.IsNullOrWhiteSpace(segment.Next))
{
IEnumerable<VariationContent> variants = product.GetVariantsContent<VariationContent>(
this._contentLoader,
this._relationRepository);
var variant = variants?.FirstOrDefault(v => v.RouteSegment.Equals(segment.Next));
// If a variant is found for the product, route to that variant instead.
if (variant != null)
{
segmentContext.RemainingPath = segment.Remaining;
segmentContext.SetCustomRouteData(ProductRoutingConstants.CurrentProductKey, routed);
segmentContext.RoutedContentLink = variant.ContentLink;
segmentContext.RoutedObject = variant;
return variant;
}
}
return routed;
}
Replace default hierarchical router
To get my custom router working I have to make sure that the default hierarchical router is not registered, by not calling CatalogRouteHelper.MapDefaultHierarchialRouter(RouteTable.Routes, false). Instead, I register my custom router in an initialization module:
[InitializableModule]
[ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))]
public class RoutingInitialization : IInitializableModule
{
private static bool _initialized;
public void Initialize(InitializationEngine context)
{
if (_initialized)
{
return;
}
var locator = context.Locate.Advanced;
// This will pick the first catalog, and strip it from all urls (in and out)
var contentLoader = locator.GetInstance<IContentLoader>();
var referenceConverter = locator.GetInstance<ReferenceConverter>();
var commerceRootLink = referenceConverter.GetRootLink();
var catalogs = contentLoader
.GetChildren<CatalogContent>(commerceRootLink);
var commerceRoot = contentLoader.Get<CatalogContentBase>(commerceRootLink);
var partialRouter = new CustomHierarchicalCatalogPartialRouter(
() => ContentReference.IsNullOrEmpty(SiteDefinition.Current.StartPage)
? SiteDefinition.Current.RootPage
: SiteDefinition.Current.StartPage,
commerceRoot,
false,
contentLoader,
locator.GetInstance<IRelationRepository>());
RouteTable.Routes.RegisterPartialRouter(partialRouter);
}
_initialized = true;
}
public void Uninitialize(InitializationEngine context)
{
}
}
Contact us