Partial Routing
Introduction
Partial routing is a new feature in CMS 7 that makes it possible to route to external data in a way where you can control the appearance of the URLs like they where “ordinary CMS pages”. The actual content displayed might by something not related to CMS at all.
CMS routes
By default content routing is registered with a pattern as "{language}/{node}/{partial}/{action}". The {language} part says that in the URL there might first be an optional part that states the language. The {node} part is used to specify the CMS page/content in the URL, it will follow the site structure and contain all page names after the start page on the site, down to the requested page. For example, in the following structure "start > news > firstNews", the URL part handled by {node} part would be "/news/firstNews". If there is something remaining in the URL after the page/content routing any registered partial router (implements interface EPiServer.Web.Routing.IPartialRouter) that matches the type of located page will get a chance to route the remaining of the URL. Then at last the any remaining part is checked if it is a valid action for a MVC controller.
IPartialRouter
A partial router is a class that implement the interface EPiServer.Web.Routing.IPartialRouter<TContent, TOutgoing> that resides in the EPiServer.dll assembly. The interface is a generic interface with generic constraints where TContent : EPiServer.Core.IContent.
The interface contains the following methods:
- RoutePartial. This method is called when the ordinary page/content routing has routed to a page/content of type TContent and there is a remaining part of the URL. The implementation can then route the remaining part of the URL.
- GetPartialVirtualPath. This method is called when an outgoing URL is constructed for a content instance of type TOutgoing.
The IPartialRouter interface is a generic interface with two parameters that is to be seen as incoming is “I can route URLs beyond instances of this type” (note that we check if type is assignable from meaning a exact type match is not required). And during outgoing construction the implementation will be called whenever an instance of TOutgoing type should state the partial URL and the reference for the page that states the base URL.
Relate implementation
There is an example of an IPartialRouter implementation in Relate templates where Blogs and Blog entries from Community is using partial routing to handle the URLs. If you look at the URLs for a blog or a blog entry on a Relate 6 site you can see that it has a URL like http://relate.episerver.com/en/Blog/My-Blog/?userId=14&entryId=11 while on a Relate 7 site the URL is something like http://localhost/HeadRelate/en/Blog/My-Blog/aUserName/SomePost/. So instead of using query string parameters for user and entryId the username of the author and the header of post is outputted as URL segments.
In relate there is a typed page MyBlog (inherits PageData) that is used as root page for all blogs and blog entries. Then there is a partial router implemented to handle routing beyond the MyBlog type. The class declaration looks like:
public class BlogPartialRouter : IPartialRouter<MyBlog, Blog>, IPartialRouter<MyBlog, Entry>
We see that the class implements the IPartialRouter interface for both EPiServer.Community.Blog.Blog and EPiServer.Community.Blog.Entry which means it routes both to blogs and blog entries. It could of course have been implemented as two separate classes instead.
The implementations of GetPartialVirtualPath (that is used to generate outgoing links) looks like:
public PartialRouteData GetPartialVirtualPath(Blog blog, string language, RouteValueDictionary routeValues, RequestContext requestContext)
{
return new PartialRouteData()
{
BasePathRoot = EPiServer.Templates.RelatePlus.Pages.Base.PageBase.MyBlogPage.PageLink,
PartialVirtualPath = String.Format("{0}/", SlugEncode(((UserAuthor)blog.Author).User.UserName))
};
}
public PartialRouteData GetPartialVirtualPath(Entry blogEntry, string language, RouteValueDictionary routeValues, RequestContext requestContext)
{
return new PartialRouteData()
{
BasePathRoot = EPiServer.Templates.RelatePlus.Pages.Base.PageBase.MyBlogPage.PageLink,
PartialVirtualPath = String.Format("{0}/{1}/",
SlugEncode(((UserAuthor)blogEntry.Blog.Author).User.UserName), SlugEncode(blogEntry.Header.Trim()))
};
}
BasePathRoot specifies which CMS page that should be used to specify the first part of the URL. In the relate package there is a property on startpage that states which page is the root page for blogs, a reference to that page is set here. PartialVirtualPath is the URL segment for the blog or entry.
The implementation of RoutePartial (that is used to route incoming request) looks like:
/// <summary>
/// Routes incoming request below CMS pages of type MyBlog. The format it supports is 'username/Header/'
/// where Header part is optional. If header part is missing the request is routed to the blog for the
/// given user otherwise it is routed to the specific entry for the given user.
/// </summary>
public object RoutePartial(MyBlog content, Web.Routing.Segments.SegmentContext segmentContext)
{
//Expected format is Name/<otional>Header/
var namePart = segmentContext.GetNextValue(segmentContext.RemainingPath);
if (!String.IsNullOrEmpty(namePart.Next))
{
var user = _securityHandler.GetUserByUserName(SlugDecode(namePart.Next));
if (user != null)
{
var blog = _myPageHandler.GetMyPage(user).Blog;
var remaingPath = namePart.Remaining;
segmentContext.SetCustomRouteData<Blog>(BlogPostKey, blog);
//Check if the optional Header part is present
var headerPart = segmentContext.GetNextValue(namePart.Remaining);
if (!String.IsNullOrEmpty(headerPart.Next))
{
var blogEntry = GetBlogEntry(blog, headerPart);
if (blogEntry != null)
{
remaingPath = headerPart.Remaining;
segmentContext.SetCustomRouteData<Entry>(BlogEntryKey, blogEntry);
}
}
//Update RemainingPath on context.
segmentContext.RemainingPath = remaingPath;
//We do not change the page which is routed to. Instead we set routed blog/entry on request
//through SetCustomRouteData which can then be consumed from MyBlog page.
return content;
}
}
return null;
}
The object returned from RoutePartial is the object that what further routing will use. Meaning that is what will be used to query TemplateResolver for a template (see post Rendering of content about rendering). So in case we would have templates registered for blog and entry, (that is either MVC controllers or WebForm pages registered with interface IRenderTemplate<Blog> and IRenderTemplate<Entry>) the blog or the entry should have been returned. However in the Relate package the template for MyBlog is used to display both blog and blog entries therefore that page is returned. Instead method SetCustomRouteData is used to pass in the blog and the entry. Then in the template corresponding method GetCustomRouteData is used to get the routed blog or entry.
To generate outgoing URLs for blogs and entries an extension method GetVirtualPathForPartialRouted on RouteCollection is used as:
RouteTable.Routes.GetVirtualPathForPartialRouted(entry, null);
It is also possible to use method GetVirtualPathForPartialRouted on class EPiServer.Web.Routing.UrlResolver to generate URLs.
Follow up
It is possible to combine partial routing with a ContentProvider implementation to get edit support (in the new edit UI) on the external data. How that can be done will be presented in another post.
Great post, Johan. Quick addition (thanks again, Johan): works better when you remember to register the partial router during initialization (for instance in the Global.RoutesRegistered event handler) using the extension method to RouteTable.Routes.RegisterPartialRouter<>().>
When I call UrlResolver.Current.GetVirtualPathForNonContent with my custom object, the url it generates is "currentpage/?routedData=MyCustomDataClass" and my IPartialRouter.GetPartialVirtualPath method is never called. Wonder what I'm missing?
Bizarely, it works in edit mode (as in, my IPartialRouter is called and it generates the correct url)