Custom Episerver Commerce Product/Variant router
Mattias Olsson
2017-12-22 14:26:46
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) { } }