Custom MVC Routes
Setting up your own controllers and routes that exist alongside the Umbraco pipeline.
Documentation about how to setup your own custom controllers and routes that need to exist alongside the Umbraco pipeline
Where to put your routing logic?
There's two places you can specify your routing, depending on whether it's in the context of a package, or your own site. If it's your own site you can do it in the Program.cs
file, within the WithEndpoints
method call like so:
If you're creating a package you won't have access to the Program.cs
file. Instead you must use a composer, for an example of this, see the example below.
User defined routes
Umbraco doesn't interfere with any user defined routes that you wish to have. Your custom routes to your own custom controllers will work perfectly and seamlessly alongside Umbraco's routes.
Custom routes within the Umbraco pipeline
For a request to be considered executing in an Umbraco context, and therefore the Umbraco pipeline, it needs to have an HTTP request feature with the type UmbracoRouteValues
, all the information required for Umbraco to handle the request is stored there. The question is now, how do we add this request feature? There's three possibilities:
Do it manually - This requires that you have a custom route, controller, even middleware, and manually assign the
UmbracoRouteValues
as an HTTP request feature, however you see fit. To create anUmbracoRouteValues
object generally requires:IUmbracoContextAccessor
(to access theCleanedUmbracoUrl
),IPublishedRouter
(to create theIPublishedRequestBuilder
),IPublishedRequestBuilder
(to set the published content and to build theIPublishedRequest
),IPublishedRequest
to assign to theUmbracoRouteValues
. As you can see this is a lot of work, but luckily there's some much easier ways.Route a custom controller that implements the
IVirtualPageController
interface, assigning theUmbracoRouteValues
to the HTTP requests will then be taken care of for you.Route a custom controller with conventional routing, using the typical call to
endpoints.MapControllerRoute
, and then call.ForUmbracoPage()
with an action for finding content on whatMapControllerRoute
returns, nowUmbracoRouteValues
will automatically be applied to any request to that controller.
Don't fret if this all seems a bit overwhelming, we'll be going through an example of the last two options.
Custom route with IVirtualPageController
As mentioned, with this approach we need to implement the IVirtualPageController
interface, this interface only has one method FindContent
which accepts an ActionExecutingContext
:
It can also be helpful to inherit from the UmbracoPageController
since this includes some useful helper methods such as CurrentPage
, do however note that it is not possible to inherit from RenderController
when doing custom routes like this.
Let's create a shop controller, with an Index action showing all our products. We will also add a Product action showing custom data about the product that could exists outside Umbraco. A common approach in a scenario like this is to have a "real" Umbraco node as a starting point. In this example we will use an empty "Products" Document Type with a list view, and "Product" Document Type containing a Stock-Keeping Unit (SKU). We also need some content based on those document types, a "Products" content node, which contains two product nodes, each with their own SKU.
After that bit of setup we can go ahead and create our shop controller which inherits from UmbracoPageController
and implements IVirtualPageController
, it'll look like this:
Now you'll see that FindContent
is complaining because we're not returning anything yet, but let's start by creating our to action methods that FindContent
will find content for.
First off we have the Index method:
With this method we return the view with the content found by the FindContent
method. This can then be used to list all the children in the view with Model.Children
.
Next we have our Product method:
This method is a bit more interesting, here we get some extra data from a different source, in this case a DbContext
, but this can be anything you want, using the id we get from the route values. We use this extra data to create a custom model, wich includes the available stores, which we then render the view with.
It's important to note that this custom model must implement IPublishedContent
, to do this we inherit from the ContentModel
class, in this case our model looks like this:
What's great about this is that we can use this model as a type argument when inheriting from UmbracoViewPage
in our model like so:
Which makes the model typed, so we can access the available stores like so:
But let's get back to our controller, the last thing we need now is to implement FindContent
method so we can find content for our actions and serve it to them. First we need to be able to get our content, and properties, so we need to inject IUmbracoContextAccessor
and IPublishedValueFallback
and save them to some fields like so:
Now that we have our dependencies, and our action methods, we're finally ready to implement the FindContent
method:
We start off by getting our product root using the UmbracoContext
to get it based off its id. Next we need to figure out what action is being requested. To do this we cast the actionExecutingContext.ActionDescriptor
to a ControllerActionDescriptor
and use its ActionName
propperty. If the action name is index, we return the product root. If it's product, we try to get the SKU from the route value id
. Then we try to find the child node which matches the SKU and return that.
There's only one last thing to do. We need to register our shop controller. If you're creating a controller for your own site you can do it in the Program.cs
like so:
As you can see there's nothing Umbraco specific abouth the controller routing, it's using the default MapController
route of the EndpointRouteBuilder
, we give our mapping a name, a pattern for the controller and some default values, so if no action is specified it will default to Index
.
If you're creating a package you won't have access to the Program.cs
, so instead you can use a composer with an UmbracoPipelineFilter
like so:
With that we have our controller with a custom route within an Umbraco context.
Client-Side Requests
If the endpoint of your custom route is considered a client-side request e.g. /sitemap.xml, you will need to make a few changes to get this to work.
Define your route as before, specifying the correct client type route:
You will need to configure your route request options within your Program.cs class. For single routes:
Or it can handle multiple routes:
In your FindContent method you should still be able to access and use IUmbracoContextAccessor through standard DI:
There is currently a bug in all versions below 9.5, where this fix won't work for mapping a client-side request to an Umbraco Controller. See https://github.com/umbraco/Umbraco-CMS/issues/12083 for more details. v9.5 fixes this issue and it's recommended to update to the latest version!
Attribute routing with IVirtualPageController
One of the benefits of the IVirtualPageController
is that it allows you to use attribute routing. If you wish to use attribute routing you must use an IVirtualPageController
and decorate your controller and/or actions with the Route
attribute. If we want to convert our above example into using attribute routing we must first add the attributes to our actions:
Now all we need to do is change our routing to use EndpointRouteBuilder.MapControllers();
instead of adding a specific route.
This will give us routing that's similar to what we have in the other example. It's worth noting that there's no defaults when using attribute routing, so to allow our index action to be accessed through both /shop
and /shop/index
, we add two attributes, specifying both routes individually.
Custom route with ForUmbracoPage
Making a custom route within the Umbraco context using ForUmbracoPage
is similar to using IVirtualPageController
. The main difference is that with ForUmbracoPage
we no longer find the content from within the controller. Instead we assign the FindContent
method when routing the controller. One important thing about ForUmbracoPage
is that attribute routing is not available. To make our example from above work with ForUmbracoPage
, we want to remove any attribute routing, and no longer implement IVirtualPageController
. We'll also remove the FindContent
method, our controller will then end up looking like this:
As you can see we still inherit from UmbracoPageController
to get access to the helper method CurrentPage
, but the rest is a normal controller.
The Umbraco magic will now instead happen where we route the controller, here we will pass a Func<ActionExecutingContext, IPublishedContent>
delegate to the ForUmbracoPage
method, this delegate is then responsible for finding the content, for instance using a composer with the same logic as in the IVirtualPageController
it will look like this:
The Compose
method of our composer is much the same as any other normal routing. The one difference is that we call ForUmbracoPage
on the MapControllerRoute
where we pass in our FindContent
method. The FindContent
method is almost the same as it was in the controller in the IVirtualPageController
example, with one important difference. Since we can no longer inject our required service into the constructor, we instead request them using actionExecutingContext.HttpContext.RequestServices.GetRequiredService
. You should not save the HttpContext
or the IServiceProvider
you get from the actionExecutingContext
to a field or property on the class. The reason for this is that they will be specific to each request.
With this we have a custom routed controller within the Umbraco pipeline. If you navigate to /shop
or /shop/product/<SKU>
you will see the controllers actions being called with the content found in FindContent
.zspo
Last updated