Custom MVC routes within the Umbraco pipeline
A while ago I wrote a post on how to do custom MVC routing in Umbraco, though the end result wasn’t quite ideal. There were a few tricks required and It wasn’t perfect since there were problems with rendering macros on the resulting view, etc… This was due to not having a PublishedContentRequest object assigned to the context. So then we went ahead and created a new attribute to assign to your MVC action to resolve this: [EnsurePublishedContentRequestAttribute]
Like the last post, you can read a lot about all of this in this Our thread. With the [EnsurePublishedContentRequestAttribute] attribute you could now assign any IPublishedContent instance to a PublishedContentRequest and be sure that it was assigned to the UmbracoContext. But this still isn’t the most ideal way to go about specifying MVC routes to work within the Umbraco pipeline… so I’ve created the following implementation which works quite well.
Background
A little bit of background in to custom MVC routes and Umbraco… The reason why it is not terribly straight forward to create a custom route and have it assigned to an Umbraco node is because the node doesn’t exist at your custom route’s location.
For example, if we have this route:
//Create a custom route RouteTable.Routes.MapRoute( "test", "Products/{action}/{sku}", new { controller = "MyProduct", action = "Product", sku = UrlParameter.Optional });
Umbraco by default would have no idea what node (IPublishedContent) would be assigned to this. The way Umbraco relates a URL to an IPublishedContent instance is by a list of IContentFinder’s. A very easy way to relate a custom URL to an IPublishedContent instance is to create your own IContentFinder. Combine that with route hijacking and in many cases this would probably be enough for your custom routing needs. However, it does not solve how you would wire up custom route parameters to your controller like how MVC normally works. Like in the above routing example, you’d want to have the ‘sku’ parameter value wired up to your Action parameter.
The above route can work and be integrated into Umbraco by following some aspects of my previous blog post and use the [EnsurePublishedContentRequestAttribute], but we can make it easier…
Creating routes
The simplest way to demonstrate this new way to create MVC routes in Umbraco is to just show you an example, so here it is:
//custom route routes.MapUmbracoRoute( "test", "Products/{action}/{sku}", new { controller = "MyProduct", sku = UrlParameter.Optional }, new ProductsRouteHandler(_productsNodeId));
This is using a new extension method: MapUmbracoRoute which takes in the normal routing parameters (you can also include constraints, namespaces, etc….) but also takes in an instance of UmbracoVirtualNodeRouteHandler.
The instance of UmbracoVirtualNodeRouteHandler is responsible for associating an IPublishedContent with this route. It has one abstract method which must be implemented:
IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext)
It has another virtual method that can be overridden which will allow you to manipulate the PublishedContentRequest however you’d like:
PreparePublishedContentRequest(PublishedContentRequest publishedContentRequest)
So how do you find content to associate with the route? Well that’s up to you, one way (as seen above) would be to specify a node Id. In the example my ProductsRouteHandler is inheriting from UmbracoVirtualNodeByIdRouteHandler which has an abstract method:
IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext, IPublishedContent baseContent);
So based on all this information provided in these methods, you can associate whatever IPublishedContent item you want to the request.
Virtual content
This implementation expects any instance of IPublishedContent, so this means you can create your own virtual nodes with any custom properties you want. Generally speaking you’ll probably have a real Umbraco IPublishedContent instance as a reference point, so you could create your own virtual IPublishedContent item based on PublishedContentWrapped, pass in this real node and then just override whatever properties you want, like the page Name, etc..
Whatever instance of IPublishedContent returned here will be converted to a RenderModel for use in your controllers.
Controllers
Controllers are straight forward and work like any other routed controller except that the Action will have an instance of RenderModel mapped to it’s parameter. Example:
public class MyProductController : RenderMvcController { public ActionResult Product(RenderModel model, string sku) { //in my case, the IPublishedContent attached to this // model will be my products node in Umbraco which i // can now use to traverse to display the product list // or lookup the product by sku if (string.IsNullOrEmpty(sku)) { //render the products list if no sku return RenderProductsList(model); } else { return RenderProduct(model, sku); } } }
I have this all working well in a side project of mine at the moment. This functionality will be exposed in an upcoming Umbraco version near you :)
It’s also worth noting that all of this was accomplished outside of the Umbraco core with the publicly available APIs that currently exist. I will admit though there were a few hacks involved which of course won’t be hacks when moved into the core ;)