World is now on Opti ID! Learn more

hans.leautaud
Apr 11, 2014
  3884
(0 votes)

Resolving an URL without {node} or {action}

For a website we’re creating I had to create a route for customers who would sign in for the first time. This URL had to be simple and should contain a unique key for us to know which user was accessing the page. How and with what we created this key is not important for now.

The URL we wanted to use was https://www.website.com/welcome/{unique code}.

I tried this with the default code to register a content route:

routes.MapContentRoute(
    "welcome_page",
    "welcome/{id}",
    new { node = "home", action = "index" } );

This did not work and I submitted a forum post. The solution presented did not work for me the way I wanted. I figured out that routing was not working for me at all so I posted another forum post. Again Johan Bjornfot to the rescue. But my problem was not solved.

In my opinion the routing above should work. If a segment is not defined in the URL but it is defined in the defaults the segment still should be set. This means that in the example above the URL is /welcome/FOOBAR. My defaults are home as node and index as action. When my URL is resolved I think the node and action should be in the RouteData.

To help you understand I created this code snippet.

var a = new RouteData();
a.DataTokens.Add("node", "home");
a.Values.Add("action", "index");
a.Values.Add("id", "FOOBAR");

The node should be in the DataTokens because it is not a default routing segment. The action and id should be in the Values because these are default MVC routing segments.
When this would happen my welcome/FOOBAR URL would be accessible because it is being processed correctly by the MultiplexingRouteHandler which is used by default when you map a route with MapContentRoute. Unfortunately this is not how it works.
Resolving the node happens in the GetRoutedData method of the
MultiplexingRouteHandler. When the DataTokens does not contain a node object this line results in a exception.

ContentReference contentLink = RequestContextExtension.GetContentLink(requestContext);

The solution I found (and maybe there are better solutions and if so please tell me!) is the following:
I created a class which inherits from MultiplexingRouteHandler. I specifically inherited from this class and not from IRouteHandler because I want everything to be the same as the default routing except for a few things and I did not want to invent the wheel all over again.
This class has a lot of private methods but GetHttpHandler and GetRouteHandler are public. The GetRouteHandler method is the one we want to alter before the URL is resolved.

public override IRouteHandler GetRouteHandler(RequestContext requestContext)
{
    ContentRoute r = requestContext.RouteData.Route as ContentRoute;
    if (r == null) return base.GetRouteHandler(requestContext);
    foreach (var i in r.Defaults)
    {
         if (i.Key.Equals(RoutingConstants.NodeKey))
         {
             if (requestContext.RouteData.DataTokens.ContainsKey(i.Key)) continue;
             if (i.Value == null || String.IsNullOrWhiteSpace(i.Value.ToString())) continue;
             requestContext.RouteData.DataTokens.Add(i.Key, this.GetPageReference(i.Value.ToString()));
         }
         if (i.Key.Equals(RoutingConstants.ActionKey))
         {
             if (requestContext.RouteData.Values.ContainsKey(i.Key)) continue;
             requestContext.RouteData.Values.Add(i.Key, i.Value);
         }
     }
     return base.GetRouteHandler(requestContext); }

So what does this code do? As you can see, before the MultiplexingRouteHandler.GetRouteHandler default functionality is triggered I am altering the RouteData object in my RequestContext object.

In my case I just wanted the {node} and {action} to be in my RouteData object (because RoutingContants are not constants I couldn’t use a switch statement). Casting my Route object in RequestContext to a ContentRoute gives me the opportunity to access the Defaults which are defined in my route map.

Background information
MapContentRoute is a extension method in RouteCollectionExtensions. This method adds a ContentRoute object to the RouteCollection.

If the node key is not defined in my DataTokens, I want to add it. Unfortunately EPiServer requires the node DateToken to be a PageReference and all I got is a string of the wanted node. Therefore I wrote the GetPageReference method (again, this is the way I thought was best but if you have a better solution, please comment).

private PageReference GetPageReference(string nodeValue)
{
     PageData page = this.contentLoader.GetBySegment(ContentReference.StartPage, nodeValue, ServiceLocator.Current.GetInstance<ILanguageSelector>()) as PageData;
     return page == null ? null : page.PageLink; }

This method retrieves all children under the start page and retrieves the first page that has the correct name and returns the PageLink as PageReference.

The action should be placed in the Values object, if not present, because this is a default MVC route segment. You could alter or extend the implementation of the GetRouteHandler method if needed.

Now when I add a content route mapping in my RouteConfig I should change my RouteHandler. This can be done via the MapContentRouteParamters.

routes.MapContentRoute(
     "welcome_page",
     "welcome/{id}",
     new { node = "home", action = "index" },
     new MapContentRouteParameters
     {
         RouteHandler = new CustomRouteHandler(
             ServiceLocator.Current.GetInstance<IContentLoader>(),
             ServiceLocator.Current.GetInstance<IPermanentLinkMapper>(),
             ServiceLocator.Current.GetInstance<TemplateResolver>(),
             ServiceLocator.Current.GetInstance<LanguageSelectorFactory>(),
             ServiceLocator.Current.GetInstance<IUpdateCurrentLanguage>()
         )
     } );

Now when I enter /welcome/FOOBAR in my browser, the node “home” is being resolved and my “index” action is being hit in my HomePageController.

Hope this will help someone in the future. If you have some questions or comments about the information above please leave a message.

Apr 11, 2014

Comments

Please login to comment.
Latest blogs
Make Global Assets Site- and Language-Aware at Indexing Time

I had a support case the other day with a question around search on global assets on a multisite. This is the result of that investigation. This co...

dada | Jun 26, 2025

The remote server returned an error: (400) Bad Request – when configuring Azure Storage for an older Optimizely CMS site

How to fix a strange issue that occurred when I moved editor-uploaded files for some old Optimizely CMS 11 solutions to Azure Storage.

Tomas Hensrud Gulla | Jun 26, 2025 |

Enable Opal AI for your Optimizely products

Learn how to enable Opal AI, and meet your infinite workforce.

Tomas Hensrud Gulla | Jun 25, 2025 |

Deploying to Optimizely Frontend Hosting: A Practical Guide

Optimizely Frontend Hosting is a cloud-based solution for deploying headless frontend applications - currently supporting only Next.js projects. It...

Szymon Uryga | Jun 25, 2025

World on Opti ID

We're excited to announce that world.optimizely.com is now integrated with Opti ID! What does this mean for you? New Users:  You can now log in wit...

Patrick Lam | Jun 22, 2025

Avoid Scandinavian Letters in File Names in Optimizely CMS

Discover how Scandinavian letters in file names can break media in Optimizely CMS—and learn a simple code fix to automatically sanitize uploads for...

Henning Sjørbotten | Jun 19, 2025 |