Integrating EPiServer 7 MVC and Commerce 1 R3 - Part 2: Routing to the Product Detail Page

March 2022 Update: This post was originally written for EPiServer version 7. In 2021, Episerver (now known as Optimizely) released version 12, and Commerce was updated to version 14. This post is very outdated now, and I suggest looking into using the new versions of the platform. No content in this post has been changed for the current version.

Routing to the Product Detail Page is one of the most important components of our EPiServer 7 MVC and Commerce integration. The routing we create will define the primary path to the product's information. Since we are not going to be creating page instances for each product, we need to have some way to determine which Entry should be displayed.

This is part of a multi-post series regarding integrating EPiServer Commerce 1 R3 with an EPiServer 7 MVC site. In this post, I'll discuss the different routing methods to the Product Detail Page.

Different Routing Methods

For our example, I want the URL to our Entry to look something like this:

http://some.domain/en-US/Product/EntryCode123

There are two different ways we can route to the Product Detail Page: using a standard MVC route or using an EPIServer MVC route. There are pros and cons to each method, so I'd suggest that you define your URL strategy before you start writing code.

Defining routes

For whichever routing method we choose, we define the route in an initialization module. Let's first start by creating this file. At this point, in our initialization module, we only care about the Initialize() method.

Business/Initialization/CommerceRoutingInitialization.cs

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class CommerceRoutingInitialization : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        // Put the routing code here!
    }

    public void Uninitialize(InitializationEngine context)
    {
    }

    public void Preload(string[] parameters)
    {
    }
}

Using a standard MVC route

The first way to route to the Product Detail Page is to use a standard MVC route (using the MapRoute() method), and make the page live outside of the EPiServer context. This will provide us the most flexibility in how we define our route, but we will lose some EPiServer functionality that we have on other pages.

Since this is a standard MVC route, the code that goes in our initialization module is pretty straightforward:

// Non-EPiServer way of routing to the product detail page
RouteTable.Routes.MapRoute(
    name: "CommerceEntry",
    url: "{language}/Product/{entry}",
    defaults: new {
        controller = "ProductDetailPage",
        action = "Index",
        entry = UrlParameter.Optional });

A nice thing about using this way of routing is that we don't need to define the {action} in the URL pattern. This allows us to have a little more flexibility with how we define the URL pattern. Notice that we are also hard-coding the name of the controller.

We could also tweak our URL pattern a bit to allow for legacy URLs (like using the Entry's SEO URL, which typically uses an .ASPX extension). All we need to do is add a wildcard to the {entry} in our URL pattern:

// Using a wildcard to allow for more complex URLs
RouteTable.Routes.MapRoute(
    name: "CommerceEntry",
    url: "{language}/Product/{entry*}",
    defaults: new {
        controller = "ProductDetailPage",
        action = "Index",
        entry = UrlParameter.Optional });

This will allow us to using URLs that look like this:

http://some.domain/en-US/Product/Category/SubCategory/EntryCode123.aspx

From here, we can parse the value of the entry parameter (which in our case would be "Category/SubCategory/EntryCode123.aspx") to validate a correct path and retrieve the Entry information.

Using an EPiServer MVC route

An EPiServer MVC route is a little more involved, but keeps us in the EPiServer context. We achieve this routing method (using the MapContentRoute() method) by creating a routing segment that can match and validate our Entry (I will also show an action filter that can do the same thing in Part 3 of this series).

You'll also notice we need to provide a PageReference to the single instance of the Product Detail Page. This keeps us in the EPiServer context, as compared to the standard MVC routing method where we just point to the ProductDetailPageController.

Business/Initialization/CommerceRoutingInitialization.cs

// EPiServer way of routing to the product detail page

// Update this line so it points to your Product Detail PageReference
var productDetailPageReference = new ContentReference(123);

var segment = new EntrySegment("entry", productDetailPageReference);

var routingParameters = new MapContentRouteParameters()
{
    SegmentMappings = new Dictionary<string, ISegment>()
};

routingParameters.SegmentMappings.Add("entry", segment);

RouteTable.Routes.MapContentRoute(
    name: "CommerceEntry",
    url: "{language}/Product/{entry}/{action}",
    defaults: new { action = "index" },
    parameters: routingParameters);

You'll also notice that we define the {action} in the URL pattern. Unfortunately, this is required, and there's not a pretty/friendly way to get around it. If you want to use the Entry's SEO URL, you'll need to change the URL pattern a bit (maybe something like {language}/Product/{action}/{entry*}), and display the action in the URL:

http://some.domain/en-US/Product/Index/Category/SubCategory/EntryCode123.aspx

Even then, you may run into an issue where the URL that is displayed shows a trailing slash:

http://some.domain/en-US/Product/Index/Category/SubCategory/EntryCode123.aspx/

On top of that, you may need to tweak the EntrySegment.cs code to pull the correct {entry} value.

Business/Routing/EntrySegment.cs

To make our EPiServer MVC routing work, we need to have a routing segment. The routing segment takes the incoming URL from the context, retrieves the {entry} value, does a bit of validation, and puts data back into the context.

public class EntrySegment : SegmentBase
{
    private ContentReference _productDetailPageReference;

    public EntrySegment(string name, ContentReference contentLink) : base(name)
    {
        _productDetailPageReference = contentLink;
    }

    public override string GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values)
    {
        return null;
    }

    public override bool RouteDataMatch(SegmentContext context)
    {
        var segmentPair = context.GetNextValue(context.RemainingPath);

        var entryCode = segmentPair.Next;

        if (!string.IsNullOrEmpty(entryCode) && IsValidEntry(entryCode))
        {
            context.RemainingPath = segmentPair.Remaining;
            context.RoutedContentLink = _productDetailPageReference;

            // We need to add the entry back into the RouteData
            // so we can use it in the Product Detail Controller
            context.RouteData.Values.Add("entry", entryCode);

            return true;
        }

        return false;
    }

    private bool IsValidEntry(string entryCode)
    {
        // You will need to implement this method to return if the Entry is valid
        return true;
    }
}

This particular segment only deals with the incoming routing data for the Entry. We can also use this code to handle the outgoing routing (though in my solution, I'm not using this). For that, I'd suggest reading Joel Abrahamsson's post on custom routing

A potentially better EPiServer MVC route

My fellow colleague, Sean Sullivan, pointed me to another way to define the EPiServer MVC route. Instead of hard-coding the /Product/ path in our URL, we can use {node} to utilize the friendly URLs that you get from the EPiServer page tree. We still need to setup the routing segment and use the EntrySegment.cs code for this option:

RouteTable.Routes.MapContentRoute(
    name: "CommerceEntry",
    url: "{language}/{node}/{entry}/{action}",
    defaults: new { action = "index" },
    parameters: routingParameters);

So with this, we can either create our page links to show the page hierarchy in the URL:

http://some.domain/en-US/Some-Landing-Page/Product-Category/Product-Listing/EntryCode123

Or if you want to use a shorter URL, we can just communicate something like this:

http://some.domain/en-US/EntryCode123

Updating the ProductDetailPageController

Now that we have the routing complete, we can update the ProductDetailPageController to take our entry route data value. In my example, I'm actually binding the entry route data value to another parameter called entryCode. The reason for this will make more sense when we develop the action filter (in Part 3).

public class ProductDetailPageController : PageController<ProductDetailPage>
{
    public ActionResult Index(ProductDetailPage currentPage, [Bind(Prefix = "entry")] string entryCode)
    {
        return View();
    }
}

Next Steps

Now that routing is in place, you are free to develop the site as you wish...

But... I want to take this one step further and create an action filter for our ProductDetailPageController, so when any of the actions are hit, we know we'll have a valid Entry. I'll discuss this action filter in Part 3 of this series.