Reimond Checiches
Nov 14, 2017
  7696
(6 votes)

Episerver CMS and Commerce custom routing

Description

We needed to implement and Episerver Website that could be customized for multiple stakeholders. Each stakeholder has his own Personal Website URL, containing a Site Name segment. Because the number of the stakeholders was more than 10000, in order to reuse the Episerver content and do not create personalized content for each stakeholder, we decided to customize Episerver routing for both CMS and Commerce content.

Example

Consider that you have the next page tree in Episerver CMS for your stakeholders’ pages:

Image EditCmsTree.png

Please see the next table for exemplifying how Episerver does the default routing and how our custom routing needs to work. The same page should be retrieved for each stakeholder, but with that stakeholder segment in the URL.

Page Name

Episerver Default URL

Custom Routing URLs

Stakeholder Home

http://quicksilver.localtest.me/en/stakeholder-home/

http://quicksilver.localtest.me/en/stakeholder1/

 

http://quicksilver.localtest.me/en/stakeholder2/

Become A Stakeholder

http://quicksilver.localtest.me/en/stakeholder-home/become-a-stakeholder/

http://quicksilver.localtest.me/en/stakeholder1/become-a-stakeholder/

 

http://quicksilver.localtest.me/en/stakeholder2/become-a-stakeholder/

About Me

http://quicksilver.localtest.me/en/stakeholder-home/about-me/

http://quicksilver.localtest.me/en/ stakeholder1/about-me/

 

http://quicksilver.localtest.me/en/ stakeholder2/about-me/

Shop Now

http://quicksilver.localtest.me/en/stakeholder-home/shop-now/

http://quicksilver.localtest.me/en/ stakeholder1/shop-now/

 

http://quicksilver.localtest.me/en/ stakeholder2/shop-now/

Shopping Cart

http://quicksilver.localtest.me/en/stakeholder-home/shopping-cart/

http://quicksilver.localtest.me/en/ stakeholder1/shopping-cart/

 

http://quicksilver.localtest.me/en/ stakeholder2/shopping-cart/

Checkout

http://quicksilver.localtest.me/en/stakeholder-home/checkout/

http://quicksilver.localtest.me/en/ stakeholder1/checkout/

 

http://quicksilver.localtest.me/en/ stakeholder2/checkout/

For Commerce, the routing is different, because we need to insert the stakeholder segment instead of replacing an existing one, as it is for CMS. Please see below some example of routing for several Catalog items URLs:

Catalog Content Type

Episerver Default URL

Custom Routing URLs

Category

http://quicksilver.localtest.me/en/fashion/mens/

http://quicksilver.localtest.me/en/stakeholder1/fashion/mens/

 

http://quicksilver.localtest.me/en/stakeholder2/fashion/mens/

Subcategory

http://quicksilver.localtest.me/en/fashion/mens/mens-shoes/

http://quicksilver.localtest.me/en/stakeholder1/fashion/mens/mens-shoes/

http://quicksilver.localtest.me/en/stakeholder2/fashion/mens/mens-shoes/

Product

http://quicksilver.localtest.me/en/fashion/mens/mens-shoes/p-36127195/

http://quicksilver.localtest.me/en/stakeholder1/fashion/mens/mens-shoes/p-36127195/

http://quicksilver.localtest.me/en/stakeholder2/fashion/mens/mens-shoes/p-36127195/

Implementation

In this section, I will describe the approach that we choose and implementation details for both CMS and Commerce content custom routing.

  1. CMS Content custom routing

    In order to customize default Episerver content routing we need to implement a custom partial router, which implements the public interface IPartialRouter<TContent, TRoutedData>. Please see the next references for details about the IPartialRouter interface and its methods: IPartialRouter Interface, RoutePartial Method and GetPartialVirtualPath Method.

    1.	/*  This custom routing code will be called just for the pages  
    2.	    which are under StartPage into the CMS page tree  
    3.	    and have page type StakeholderBasePage*/  
    4.	public class CustomCmsContentPartialRouter : IPartialRouter<StartPage, StakeholderBasePage>  
    5.	    {  
    6.	        private readonly IContentLoader _contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();  
    7.	  
    8.	        public object RoutePartial(StartPage content, SegmentContext segmentContext)  
    9.	        {  
    10.	            string stakeholderSiteName;  
    11.	            // All the stakeholder pages will inherite from the same base page  
    12.	            StakeholderBasePage stakeholderPage = null;  
    13.	            var contentUrl = CustomRoutingHelper.GetStakeholderContentUrl(segmentContext, out stakeholderSiteName);  
    14.	            var contentReference = CustomRoutingHelper.TryGetContentRefrence(contentUrl);  
    15.	  
    16.	            if (contentReference != null)  
    17.	            {  
    18.	                if (CustomRoutingHelper.CheckIfCustomSiteName(segmentContext, stakeholderSiteName))  
    19.	                {  
    20.	                    stakeholderPage = _contentLoader.Get<StakeholderBasePage>(contentReference.ContentLink);  
    21.	  
    22.	                    if (stakeholderPage != null)  
    23.	                    {  
    24.	                        segmentContext.RemainingPath = string.Empty;  
    25.	                        segmentContext.RoutedContentLink = stakeholderPage.ContentLink;  
    26.	                    }  
    27.	                }  
    28.	            }  
    29.	  
    30.	            return stakeholderPage;  
    31.	        }  
    32.	  
    33.	        public PartialRouteData GetPartialVirtualPath(JewelerBasePage content, string language, RouteValueDictionary routeValues,  
    34.	            RequestContext requestContext)  
    35.	        {  
    36.	            string urlSegment;  
    37.	            string stakeholderSiteName;  
    38.	              
    39.	            if (CustomRoutingHelper.CheckIfCustomSiteName(requestContext, out stakeholderSiteName))  
    40.	            {  
    41.	                urlSegment = GetRoutedContentLink(content, stakeholderSiteName);  
    42.	            }  
    43.	            else  
    44.	            {  
    45.	                return null;  
    46.	            }  
    47.	  
    48.	            return new PartialRouteData  
    49.	            {  
    50.	                BasePathRoot = ContentReference.StartPage,  
    51.	                PartialVirtualPath = urlSegment  
    52.	            };  
    53.	        }  
    54.	  
    55.	        private string GetRoutedContentLink(StakeholderBasePage content, string stakeholderSiteName)  
    56.	        {  
    57.	            /*  Stop iteration if content type is StakeholderStartPage. 
    58.	                All pages with type StakeholderBasePage should be under  
    59.	                StakeholderStartPage into CMS page tree */  
    60.	            if (content is StakeholderStartPage)  
    61.	                return stakeholderSiteName;  
    62.	  
    63.	            var parentContent = _contentLoader.Get<StakeholderBasePage>(content.ParentLink);  
    64.	  
    65.	            return GetRoutedContentLink(parentContent, stakeholderSiteName) + "/" + content.URLSegment;  
    66.	        }  
    67.	    }
    
  2. Commerce/Catalog Content custom routing

    In order to customize default Episerver content routing we need to implement a custom partial router, which extends the public class HierarchicalCatalogPartialRouter. Please see the next references for details about the HierarchicalCatalogPartialRouter class and  the overrode methods: HierarchicalCatalogPartialRouter Class, FindNextContentInSegmentPair Method and GetPartialVirtualPath Method.

    1.	public class CustomCommerceCatalogRouter : HierarchicalCatalogPartialRouter  
    2.	{  
    3.	    public CustomCommerceCatalogRouter(CatalogContentBase commerceRoot, Func<ContentReference> func)  
    4.	    : base(func, commerceRoot, false) { }  
    5.	  
    6.	    protected override CatalogContentBase FindNextContentInSegmentPair(CatalogContentBase catalogContent, SegmentPair segmentPair, SegmentContext segmentContext, CultureInfo cultureInfo)  
    7.	    {  
    8.	  
    9.	        if (!CustomRoutingHelper.CheckIfCatalogContentUrl(segmentContext.RequestUrl.ToString()))  
    10.	            return null;  
    11.	  
    12.	        CustomRoutingHelper.SkipCustomSiteNameSegment(segmentPair, segmentContext.RequestUrl.ToString());  
    13.	        var currentCatalog = base.FindNextContentInSegmentPair(catalogContent, segmentPair, segmentContext, cultureInfo);  
    14.	  
    15.	        return currentCatalog;  
    16.	    }  
    17.	  
    18.	    public override PartialRouteData GetPartialVirtualPath(CatalogContentBase content, string language, RouteValueDictionary routeValues,  
    19.	        RequestContext requestContext)  
    20.	    {  
    21.	        string stakeholderSiteName;  
    22.	  
    23.	        var partialRouteData = base.GetPartialVirtualPath(content, language, routeValues, requestContext);  
    24.	  
    25.	        if (CustomRoutingHelper.CheckIfCustomSiteName(requestContext, out stakeholderSiteName))  
    26.	        {  
    27.	            partialRouteData.PartialVirtualPath = stakeholderSiteName + "/" + partialRouteData.PartialVirtualPath;  
    28.	        }  
    29.	  
    30.	        return partialRouteData;  
    31.	    }  
    32.	}  
    
  3. Common methods from Helpers

    CustomRoutingHelper.GetStakeholderContentUrl method will return the Episerver Content URL. This method will replace stakeholder site name with stakeholder Episerver home page URL, in return in the stakeholderSiteName output parameter the stakeholder custom site name.

    E.g.: For http://website/stakeholder-sitename the method will return http://website/stakeholder-home and stakeholderSiteName = "stakeholder-sitename".

    Please see below the CustomRoutingHelper class:

    1.	public class CustomRoutingHelper  
    2.	{  
    3.	    public static string GetStakeholderContentUrl(SegmentContext segmentContext, out string stakeholderSiteName)  
    4.	    {  
    5.	        var nextSegment = segmentContext.GetNextValue(segmentContext.RemainingPath);  
    6.	  
    7.	        if (CultureHelper.IsAvailableCulture(nextSegment.Next))  
    8.	        {  
    9.	            nextSegment = segmentContext.GetNextValue(nextSegment.Remaining);  
    10.	        }  
    11.	  
    12.	        stakeholderSiteName = nextSegment.Next;  
    13.	        var regex = new Regex(Regex.Escape(nextSegment.Next));  
    14.	        // replace custom site name with CMS stakeholder HomePage/StartPage URL  
    15.	        return regex.Replace(segmentContext.RequestUrl.AbsolutePath, "stakeholder-home", 1);  
    16.	    }  
    17.	  
    18.	    // check if request path cotains a valid stakeholder custom site name  
    19.	    public static bool CheckIfCustomSiteName(RequestContext requestContext, out string stakeholderSiteName)  
    20.	    {  
    21.	        stakeholderSiteName = null;  
    22.	  
    23.	        if (requestContext.HttpContext == null)  
    24.	            return false;  
    25.	  
    26.	        // ajax requests got stakeholder ID in request headers  
    27.	        if (requestContext.HttpContext.Request.IsAjaxRequest())  
    28.	            return false;  
    29.	  
    30.	        var requestPath = requestContext.HttpContext.Request.Path;  
    31.	        var segments = requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);  
    32.	  
    33.	        if (segments.Length > 0)  
    34.	        {  
    35.	            if (CultureHelper.IsAvailableCulture(segments[0]) && segments.Length > 1)  
    36.	            {  
    37.	                stakeholderSiteName = segments[1];  
    38.	            }  
    39.	            else  
    40.	            {  
    41.	                stakeholderSiteName = segments[0];  
    42.	            }  
    43.	  
    44.	            return CheckIfStakeholderSiteName(stakeholderSiteName, requestPath);  
    45.	        }  
    46.	  
    47.	        return false;  
    48.	    }  
    49.	  
    50.	    public static bool CheckIfCustomSiteName(SegmentContext segmentContext, string stakeholderSiteName)  
    51.	    {  
    52.	        return CheckIfStakeholderSiteName(stakeholderSiteName, segmentContext.RequestUrl.ToString());  
    53.	    }  
    54.	  
    55.	    public static void SkipCustomSiteNameSegment(SegmentPair segmentPair, string requestUrl)  
    56.	    {  
    57.	        var remainingSegments = segmentPair.Remaining.Split('/');  
    58.	  
    59.	        if (remainingSegments.Length <= 0   
    60.	            ||!remainingSegments[0].Equals("DefaultCatalogName", StringComparison.OrdinalIgnoreCase)   
    61.	            || !CheckIfStakeholderSiteName(segmentPair.Next, requestUrl))   
    62.	                return;  
    63.	  
    64.	        segmentPair.Next = remainingSegments[0];  
    65.	        segmentPair.Remaining = segmentPair.Remaining.Remove(0, remainingSegments[0].Length + 1);  
    66.	    }  
    67.	  
    68.	    // check if the segment is a valid stakeholder site name  
    69.	    public static bool CheckIfStakeholderSiteName(string segment, string requestUrl)  
    70.	    {  
    71.	        if (segment.IsNullOrEmpty()) return false;  
    72.	  
    73.	        var key = new Tuple<string, string>(segment.ToLower(), requestUrl);  
    74.	  
    75.	        // avoid extra checks for already checked segments  
    76.	        if (StakeholderSiteNames.ContainsKey(key))  
    77.	            return StakeholderSiteNames[key];  
    78.	  
    79.	        // check for some specific routes to avoid extra call to the database  
    80.	        if (segment.Equals("episerver", StringComparison.OrdinalIgnoreCase)  
    81.	            || segment.Equals("stakeholder-home", StringComparison.OrdinalIgnoreCase)  
    82.	            || segment.Equals("DefaultCatalogName", StringComparison.OrdinalIgnoreCase))  
    83.	            return false;  
    84.	  
    85.	        bool exists = //check if there is a custom sitename in your system equal with the segment   
    86.	  
    87.	        StakeholderSiteNames.Add(key, exists);  
    88.	  
    89.	        return exists;  
    90.	    }  
    91.	  
    92.	    // This dictionary help us to avoid extra checks in the same request  
    93.	    private static Dictionary<Tuple<string, string>, bool> StakeholderSiteNames  
    94.	    {  
    95.	        get  
    96.	        {  
    97.	            if (HttpContext.Current.Items["StakeholderSiteNames"] == null)  
    98.	                HttpContext.Current.Items["StakeholderSiteNames"] = new Dictionary<Tuple<string, string>, bool>();  
    99.	            return HttpContext.Current.Items["StakeholderSiteNames"] as Dictionary<Tuple<string, string>, bool>;  
    100.	        }  
    101.	    }  
    102.	  
    103.	    public static bool CheckIfCatalogContentUrl(string requestUrl)  
    104.	    {  
    105.	        return requestUrl.Contains("DefaultCatalogName");  
    106.	    }  
    107.	  
    108.	    public static IContent TryGetContentRefrence(string url)  
    109.	    {  
    110.	        try  
    111.	        {  
    112.	            return UrlResolver.Current.Route(new UrlBuilder(url));  
    113.	        }  
    114.	        catch  
    115.	        {  
    116.	            return null;  
    117.	        }  
    118.	    }  
    119.	}  
    


    Please see below the CultureHelper class:

    1.	public class CultureHelper  
    2.	    {  
    3.	        public static bool IsAvailableCulture(string cultureCode)  
    4.	        {  
    5.	            var availableLanguages = GetAvailableLanguages();  
    6.	  
    7.	            return availableLanguages.Contains(cultureCode);  
    8.	        }  
    9.	  
    10.	        private static IList<string> GetAvailableLanguages()  
    11.	        {  
    12.	            var languageBranchRepository = ServiceLocator.Current.GetInstance<ILanguageBranchRepository>();  
    13.	            var availableLanguages = languageBranchRepository.ListEnabled();  
    14.	  
    15.	            if (availableLanguages != null && availableLanguages.Any())  
    16.	                return availableLanguages.Select(x => x.URLSegment).ToList();  
    17.	  
    18.	            return new List<string>();  
    19.	        }  
    20.	    }  
    
Nov 14, 2017

Comments

Quan Mai
Quan Mai Nov 14, 2017 10:43 AM

Nice - I haven't jumped into detail, but I appreciate the time and effort you put into write such a long, well formatted post. Keep it up :) 

Please login to comment.
Latest blogs
Optimizely’s Sustainability Journey: Creating a Future That’s Built to Last

  For me, sustainability isn’t just a company priority; it’s a personal one. I’m especially mindful of the role that content management plays at th...

Joey Moore | Dec 13, 2024

How I Fixed DLL Conflicts During EPiServer CMS Upgrade to .NET Framework 4.8.1

We had a CMS solution of EPiServer 11.26.0, which was built on .NET Framework 4.7.1. We needed to update the target framework from .NET Framework...

calimat | Dec 12, 2024

Custom form element view in Optimizely CMS 12

Do you want full control over the form element markup? Create your own views!

Tomas Hensrud Gulla | Dec 11, 2024 | Syndicated blog

How to Elevate Your Experimentation - Opticon workshop experience

As a non-expert in the field of experimentation, I’d like to share my feedback on the recent Opticon San Antonio workshop session titled "How to...

David Ortiz | Dec 11, 2024

Persisting a Strawberry Shake GraphQL Client for Optimizely's Content Graph

A recent CMS project used Strawberry Shake to generate an up-to-date C# GraphQL client at each build. But what happens to the build if the GraphQL...

Nicholas Sideras | Dec 11, 2024 | Syndicated blog

Opti ID with Secure Cookies And Third Party AddOns

Opti ID has revolutionised access to the Optimizely One suite and is now the preferred authentication method on all PAAS CMS websites that I build....

Mark Stott | Dec 9, 2024