Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
All about Umbraco's routing pipeline & the types of Controllers used in Umbraco
All about Umbraco's routing pipeline & the types of Controllers used in Umbraco, how they work, and what they are used for.
Explains how Umbraco builds its URLs and how the URLs are mapped back to content items.
Describes the Umbraco special/reserved Property Type aliases that can be used to directly manipulate Umbraco's default routing pipeline. These special Property Type aliases can be useful when creating an Umbraco website.
What is a Surface Controller and how to use them?
What is an API Controller and how to use them?
Creating custom controllers to have 100% full control over how your pages are rendered. Also known as: Hijacking Umbraco Routes
How to specify your own custom MVC routes in your Umbraco application?
Routing requirements for authenticated controllers for both front-end and the backoffice.
Moving and renaming Umbraco documents will lead to URL redirects to be created.
Describes special property type aliases which can be used to customise routing
There are a few special/reserved Umbraco Property Type aliases that can be used which can manipulate how the standard Umbraco routing pipeline works. You can add these Property Types to any Document Type and if values are assigned to these properties, Umbraco will adjust its routing accordingly. See below for full details.
Creating a property alias with this name and using a Content Picker property editor lets you create a 302 temporary redirect. This in effect means that when a user navigates to this node, they will be redirected away from it.
Add this property alias to your Document Type with a Content Picker property editor and Umbraco will load the selected page’s content transparently without performing any URL redirection. This essentially performs a rewrite.
This property when created as a text string lets you provide a different URL name to what is created by default by the name of the node. If you enter a value for this property and save/publish the content node you will see that its main URL is updated with a new path suffix.
This property when created as a text string lets you provide a comma separated list of alternate full URL paths for the node. For example, if your URL was /some-category/some-page/content-node, by adding an umbracoUrlAlias of "flowers", a user can navigate to the node by going to /flowers. The URL alias remains in the browser address bar as a 'mask' over the real URL. You can also specify paths like "flowers/roses/red".
Use a custom MVC controller to handle and control incoming requests for content pages based on a specific Document Type, also called Route Hijacking.
Use a custom controller to handle and control incoming requests for content pages based on a specific Document Type
By default, all front end requests to an Umbraco site are auto-routed via the 'Index' action of a core Controller: Umbraco.Cms.Web.Common.Controllers.RenderController
. This core controller handles the incoming request, builds the associated PublishedContent model, and passes this to the appropriate Umbraco Template/MVC View.
It is however possible to implement a custom Controller to replace this default implementation to give complete control over this execution.
For example:
To enrich the view model passed to the template with additional properties (from other published content items or outside Umbraco)
To implement serverside paging
To implement any custom/granular security
To return alternative templates depending on some custom business logic
This replacement of the default controller can be made 'globally' for all requests (see last example). It can also be by 'hijacking' requests for types of pages based on their specific Document Type following this controller naming convention: [DocumentTypeAlias]Controller
.
In the following example, imagine an Umbraco site with a set of 'product' pages created from a Document Type called 'Product Page' with an alias 'productPage'.
Create a custom locally declared controller in the Umbraco web application project named 'ProductPageController'.
Ensure that this controller inherits from the base controller Umbraco.Cms.Web.Common.Controllers.RenderController
.
eg:
All requests to any 'product' pages in the site will be'hijacked' and routed through the custom ProductPageController.
If you prefer to use an async controller your need to override both the sync and the async Index()-methods. This is done to disable the default behavior from the base controller.
This example shows the default behaviour that Umbraco's core RenderController provides. The 'Index' action of the controller is executed, and the CurrentTemplate helper sends the model containing the details of the published content item related to the request to the relevant template/view.
A further convention is that if an action on the controller has a name that matches the template name, this action will be executed instead of the default 'Index' action.
In this example, the Product Page Document Type has two templates 'ProductPage' and 'ProductAmpPage'. We can hijack and handle the requests to the two templates differently.
Create the Controller as before:
The page in Umbraco will have a single 'template' selected as it's default template, but it's possible to call this same page on a different template by adding ?altTemplate=othertemplatename
to the Url QueryString eg:
/products/superfancyproduct/?altTemplate=ProductAmpPage
Document Type name = controller name
Template name = action name (if no action matches or is not specified - then the 'Index' action will be executed).
Controller Inherits from Umbraco.Cms.Web.Common.Controllers.RenderController
The steps to achieve this will differ, depending if your template views are using IPublishedContent or Modelsbuilder generated Models.
By default, your Umbraco Template will be based on the ContentModel
that the default RenderController
passes through to it.
The default inherits statement:
or if you are using modelsbuilder:
<>
contains a model generated for each document type to give strongly typed access to the Document Type properties in the template view.
To use a specific custom view model, the @inherits
directive will need to be updated to reference your custom model using the Umbraco.Cms.Web.Common.Views.UmbracoViewPage<T>
format where 'T' is the type of your custom model.
So for example, if your custom model is of type 'MyProductViewModel' then your @inherits
directive will look like:
Views will likely specify a master view to use as the common layout for the site html. When using a custom view model it's necessary to make sure this doesn't conflict with any implementation in the master layout view. Eg. if your master layout view is inheriting from a specific model UmbracoViewPage<SpecificModel>
and using a property from SpecificModel that isn't available in your custom model an exception will be thrown. To avoid this you could:
Keep your Master layout view 'generically typed', eg. only have @inherits UmbracoViewPage
, and use Model.Value syntax to access properties. or
Break the dependency on Umbraco.Cms.Core.Models
in your master layout by having it instead inherit from Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ISomeInterface>
. This would be where ISomeInterface is implemented by all your models and contains the properties that the master layout view uses. or
Ensure your custom models inherit from whichever class is used to strongly type the master layout view.
In most cases you will need your custom model to build upon the underlying existing PublishedContent model for the page. This can be achieved by making your custom model inherit from a special base class called PublishedContentWrapped
:
PublishedContentWrapped
will take care of populating all the usual underlying Umbraco properties and means the @Model.
syntax will continue to work in the layouts used by your template.
Using Modelsbuilder you will find that all the generated models have a constructor that takes an IPublishedContent item in a similar way:
The models generated by Modelsbuilder are created as partial classes so it's possible to extend them by adding your own partial classes with matching signature.
We can now populate our custom view model in our controller and use the values from the custom model in our template view:
and in our template
You can also pass values directly into the controller action using the query string.
The values in the querystring will be bound to the matching parameters defined in the controller's action:
Injecting services into your controller constructors is possible with Umbraco's underlying dependency injection implementation. See Services and Helpers for more info on this.
For example:
To wire up a concrete instance of IMadeUpProductService, use a composer:
See Composing for further information.
RenderController
You can replace Umbraco's default implementation of RenderController with your own custom controller for all MVC requests. This is possible by assigning your own default controller type in the Umbraco setup during initialization.
You can achieve this by updating the options for UmbracoRenderingDefaultsOptions
in the ConfigureServices
method in the Startup.cs
class.
First of all you have to create your own controller. Your custom implementation of RenderController should either inherit from the core RenderController
as in the examples above or implement the IRenderController
interface.
Implement the IRenderController
:
Or inherit from RenderController
The last step is to configure Umbraco to use your implementation. You can do that in the ConfigureServices
method in the Startup.cs
class.
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
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 Configure
method of Startup.cs
within the WithEndpoints
method call like so:
If you're creating a package you won't have access to the Startup.cs
file, so instead you must use a composer, for an example of this, see the example below.
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.
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 completely 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 an UmbracoRouteValues
object generally requires: IUmbracoContextAccessor
(to access the CleanedUmbracoUrl
), IPublishedRouter
(to create the IPublishedRequestBuilder
), IPublishedRequestBuilder
(to set the published content and to build the IPublishedRequest
), IPublishedRequest
to assign to the UmbracoRouteValues
. As you can see this is quite a lot of work, but luckily there's some much easier ways.
Route a custom controller that implements the IVirtualPageController
interface, assigning the UmbracoRouteValues
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 what MapControllerRoute
returns, now UmbracoRouteValues
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.
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, and an Product action which will show some 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're going to use an empty "Products" document type to act as a list view, and "Product" document type which only contains an 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:
This is a fairly straightforward method, we return the view with the content found by the FindContent
method, which 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 just return the product root, but if it's product, we try to get the SKU from the route value id
, and try to find the child node which matches the SKU and return that.
Now 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 Configure
method of Startup.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 Startup.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.
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 Startup.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:
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.
Making a custom route within the Umbraco context using ForUmbracoPage
is quite 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, so to make our example from above work with ForUmbracoPage
, we want to remove any attribute routing, and no longer implement IVirtualPageController
, so 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, with one difference we call ForUmbracoPage
on the MapControllerRoute
where we pass in our FindContent
method. The FindContent
method is also largely 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
. It's important to note here that you should not save the HttpContext
or the IServiceProvider
you get from the actionExecutingContext
to a field or property on the class since these will be specific for 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
URL redirect management in Umbraco
Whenever a document is published, and this causes changes to its URL (and any of its descendants' URLs), Umbraco makes a note of the old URLs. Whenever an incoming request is served and the default content finders cannot find a matching published document, Umbraco checks whether the URL matches one of these saved URLs. If a match is found, Umbraco returns a "301 Redirect" response pointing to the new URL of the document.
The URL Redirect Management functionality does not support rewriting "rules" (e.g. regular expressions), nor complex scenarios (e.g. changing the culture and hostnames configuration). There are already powerful solutions to deal with these types of situations, such as Microsoft's own Url Rewrite module for IIS. Since netcore is decoupled from the webserver hosting it, your approach for URL rewriting, outside what Umbraco provide out of the box, will depend on what you use to host your solutions, but for more info on the IIS Url Rewrite module have a look at the official documentation.
It is possible to list the redirect URLs via the Redirect Url Management dashboard in the Content section. This dashboard lists the original URL, new URL, and culture. It also allows you to delete a URL redirect.
In addition, the dashboard can be used to disable or enable the 301 Redirect Management (via the appsettings.json
configuration option described below - note that this requires an application restart to take effect).
Anytime a document is published and its corresponding url segment changes, Umbraco checks its URL (and all its descendants' URLs) for changes. For every URL that has changed, it creates (or updates) a row in the umbracoRedirectUrl
table. Rows in this table contain: the old url, the create date, and the target content identifier, culture, and a url hash.
Umbraco registers a new content finder, ContentFinderByRedirectUrl
, which runs as a normal content finder after the other content finders. It looks for the incoming URL in the database table and, if found, computes the URL of the target document and returns a "301 Redirect". These redirects are considered "permanent". It's good to note that we explicitly set no-cache
headers on these redirects so that when they change, browsers update the URL immediately. They are a "true" 301, however, and search engines will accept them as such.
The 301 Redirect Management feature is enabled by default.
It is possible to disable the feature entirely (both generating URLs in the database table, and running the content finder) by editing the appsettings.json
file:
See the web routing config reference for more configuration options
Requirements for authenticating requests for the backoffice
In order for Umbraco to authenticate a request for the backoffice, the routing needs to be specific. Any URL that routes to:
/umbraco/backoffice/*
will be authenticated. If you have a controller that is not routed within the prefix, it will not be authenticated for backoffice use.
You do not have to worry about routing if you are using Umbraco.Cms.Web.BackOffice.Controllers.UmbracoAuthorizedApiController
(or any inherited controller) since these are auto routed. All implementations of UmbracoAuthorizedApiController
(which includes UmbracoAuthorizedJsonController
) are auto-routed with the default route:
/umbraco/backoffice/api/{controller}/{action}
In the case that an Umbraco Api Controller is a 'Plugin Controller', then the route would be:
/umbraco/backoffice/{pluginname}/{controller}/{action}
The {area} specified by the [PluginController] attribute replaces the /api/ area for the route.
Depending on the type of controller used (MVC or WebAPI), the controller is not auto-routed. You will need to declare a custom route and register it with the Umbraco DI container to make it work.
For more information on authenticated/authorized controllers & attributes see the Controllers Documentation.
Defining a route is done with the standard .NET Core MVC routing practices, however there is a handy extension method on the IEndpointRouteBuilder
to help you.
When creating custom routes you can either do it directly in the Startup.cs
files, or with a pipeline filter in a composer which looks something like:
The signature of MapUmbracoRoute<T>
is as follows
The generic type argument is the contoller you wish to route, in this case MyController
.
rootSegment
- The first part of the pattern, since this is an authorized controller it has to be umbraco/backoffice
.
areaName
- The name of the area the controller should be routed through, an empty string signifies no area.
prefixPathSegment
- Prefix to be applied to the rootSegment, we know this from api controllers where the prefix is api
, in this case since the controller is in an area we will also prefix the area name to the URL, so the final path pattern will be umbraco/backoffice/mypackagename/{controllerName}/{action}/{id?}
.
defaultAction
- If this is not null or an empty string the request will automatically be routed to the specified action, so in this case umbraco/backoffice/mypackagename/{controllerName}
will route to the index action.
includeControllerNameInRoute
- If this is false the controller name will be excluded from the route, so in this case the route would be umbraco/backoffice/mypackagename/{action}/{id?}
if this was set to false.
constraints
- Any routing constraints passed to this will be used when mapping the route see Microsoft documentation for more information.
Using the MapUmbracoRoute
extension method is optional though, it's a neat helper to ensure controllers get routed in the same way. If your controller uses an area, like in this example, you need to specify this using the Area
attribute. In this example the controller looks like this:
The route must be prefixed with the Umbraco path, which is configurable and resolved with GetUmbracoMvcArea()
from IGlobalSettings
. Then, it should be followed by "/backoffice" in order for Umbraco to check user authentication.
Surface Controllers should not be used in the backoffice. Surface Controllers are not designed to work with the backoffice. They are not meant to be used there and will not be supported being used there.
With the release of Umbraco 9 and the change of the underlying web framework that is decoupled from the webserver, the way that you configure rewrites has changed as well.
Instead of the URL Rewriting extension in IIS you can use the URL Rewriting Middleware in ASP.NET Core, which needs to be added to your project startup code first.
If you are running Umbraco 9 on IIS you can still add a web.config
file to configure IIS features such as URL rewrites.
Make sure to check the official URL Rewriting Middleware in ASP.NET Core documentation for more information about when you should or should not use the URL Rewriting Middleware.
To use rewrites with Umbraco 9 you have to register the middleware in your Startup.cs
by using the UseRewriter
extension method and then configure the rewrite options.
Create an IISUrlRewrite.xml
file in the root of your project (next to your Startup.cs
file) containing:
In the Startup.cs
file you can add the URL Rewriting Middleware just before the call to app.UseUmbraco()
and use AddIISUrlRewrite
) to add the rewrite rules from the XML file:
On Linux, make sure to place the app.UseStaticFiles()
after the app.UseUmbraco()
statements for the redirect to work as intended.
In your csproj file add the XML file to a new item group and set CopyToOutputDirectory
to Always
:
On Umbraco Cloud the item group needs to be set to <CopyToPublishDirectory>Always</CopyToPublishDirectory>
for the file to be published to your deployed site.
RewriteOptions
has a number of "shortcut" methods to implement commonly used rewrites including:
AddRedirectToNonWww()
AddRedirectToWww()
AddRedirectToNonWwwPermanent()
AddRedirectToWwwPermanent()
AddRedirectToHttps()
For more details and other examples, take a look at the URL Rewriting Middleware in ASP.NET Core and RewriteOptions Class documentation.
A great site showing 10 very handy IIS Rewrite rules: URL rewriting tips and tricks
Another site showing some handy examples of IIS Rewrite rules: Some useful IIS rewrite rules
If you needed to a lot of static rewrites using rewrite maps: Rule with rewrite map rule template
For example, to always remove a trailing slash from the URL (make sure Umbraco doesn't add a trailing slash to all generated URLs by setting AddTrailingSlash
to false
in your RequestHandler settings):
Another example would be to enforce HTTPS only on your site:
Another example would be to redirect from non-www to www (except for the Umbraco Cloud project hostname):
If you use Umbraco Cloud check the Rewrite Rules article.
Information about creating your own content finders
To create a custom content finder, with custom logic to find an Umbraco document based on a request, implement the IContentFinder interface:
and use either an Umbraco builder extension, or a composer to add it to it to the ContentFindersCollection
.
Umbraco runs all content finders in the collection 'in order', until one of the IContentFinders returns true. Once this occurs, the request is then handled by that finder, and no further IContentFinders are executed. Therefore the order in which ContentFinders are added to the ContentFinderCollection is important.
The ContentFinder can set the PublishedContent item for the request, or template or even execute a redirect.
This IContentFinders will find a document with id 1234, when the Url begins with /woot.
You either use an extension on the Umbraco builder or, a composer to access the ContentFinderCollection
to add and remove specific ContentFinders
First create the extension method:
Then invoke it in ConfigureServices
in the Startup.cs
file:
To set your own 404 finder create an IContentLastChanceFinder and set it as the ContentLastChanceFinder. (perhaps you have a multilingual site and need to find the appropriate 404 page in the correct language)
A IContentLastChanceFinder
will always return a 404 status code. This example creates a new implementation of the IContentLastChanceFinder
and gets the 404 page for the current language of the request.
You can configure Umbraco to use your own implementation in the ConfigureServices
method of the Startup
class in Startup.cs
:
The followed method is called on the "PublishedContentRequest.PrepareRequest()" method: FindPublishedContentAndTemplate()
. We discuss shortly what this method is doing:
FindPublishedContent ()
Handles redirects
HandlePublishedContent()
FindTemplate()
FollowExternalRedirect()
HandleWildcardDomains()
No content?
Run the LastChanceFinder
Is an IContentFinder, resolved by ContentLastChanceFinderResolver
By default, is null (= ugly 404)
Follow internal redirects
Take care of infinite loops
Ensure user has access to published content
Else redirect to login or access denied published content
Loop while there is no content
Take care of infinite loops
Use altTemplate if
Initial content
Internal redirect content, and InternalRedirectPreservesTemplate is true
No alternate template?
Use the current template if one has already been selected
Else use the template specified for the content, if any
Alternate template?
Use the alternate template, if any
Else use what’s already there: a template, else none
Alternate template is used only if displaying the intended content
Except for internal redirects
If you enable InternalRedirectPreservesTemplate
Which is false by default
Alternate template replaces whatever template the finder might have set
ContentFinderByNiceUrlAndTemplate
/path/to/page/template1?altTemplate=template2 template2
Alternate template does not falls back to the specified template for the content
/path/to/page?altTemplate=missing no template
Even if the page has a template
But preserves whatever template the finder might have set
/path/to/page/template1?altTemplate=missing template1
Content.GetPropertyValue("umbracoRedirect")
If it’s there, sets the published content request to redirect to the content
Will trigger an external (browser) redirect
Finds the deepest wildcard domain between
Domain root (or top)
Request’s published content
If found, updates the request’s culture accordingly
This implements separation between hostnames and cultures
Information about Surface Controllers in Umbraco
A surface controller is an MVC controller that interacts with the front-end rendering of an Umbraco page. They can be used for rendering view components and for handling form data submissions. Surface controllers are auto-routed, meaning that you don't have to add/create your own routes for these controllers to work.
It is a regular ASP.NET Core MVC controller that:
Is auto-routed, meaning you don't have to setup any custom routes to make it work
Is used for interacting with the front-end of Umbraco (not the backoffice)
Since any surface controller inherits from the Umbraco.Cms.Web.Website.Controllers.SurfaceController
class, the controller instantly supports many of the helper methods and properties that are available on the base SurfaceController
class including UmbracoContext
. Therefore, all surface controllers have native Umbraco support for:
Interacting with Umbraco routes during HTTP POSTs (i.e. return CurrentUmbracoPage();
)
Rendering forms in Umbraco (i.e. @using (Html.BeginUmbracoForm<MyController>(...)){}
)
Rendering of ASP.NET Core MVC view components
Surface controllers are plugins, meaning they are found when the Umbraco application boots. There are 2 types of surface controllers: locally declared & plugin based. The main difference between the two is that a plugin based controller gets routed via an MVC area, which is defined in the controller (see below). Because a plugin based controller is routed via an MVC area, it means that the views can be stored in a custom folder specific to the package it is being shipped in. This can be done without interfering with the local developer's MVC view files.
A locally declared surface controller is one that is not shipped within an Umbraco package. It is created by the developer of the website they are creating. If you are planning on shipping a surface controller in an Umbraco package, then you will need to create a plugin based surface controller (see the next heading).
To create a locally declared surface controller:
Create a controller that inherits from Umbraco.Cms.Web.Website.Controllers.SurfaceController
The controller must be a public class.
The controller must call the base constructor of SurfaceController
The controller's name must be suffixed with the term Controller
The controller must be inside a namespace
For example:
All locally declared controllers gets routed to:
They do not get routed via an MVC area, so any views must exist in the following folders:
/Views/{controllername}/
/Views/Shared/
/Views/
If you get a 404 error when trying to access your surface controller, you may have forgotten to add a namespace to it!
If you are shipping a surface controller in a package, then you should definitely be creating a plugin based surface controller. The only difference between creating a plugin based controller and locally declared controller, is that you need to add an attribute to your class, which defines the MVC area you'd like your controller to be routed through. Here's an example:
In the above, the surface controller will belong to the MVC area called 'SurfaceControllerPackage'. Perhaps it is obvious, but if you are creating a package that contains many surface controllers, then you should most definitely ensure that all of your controllers are routed through the same MVC area.
All plugin based controllers get routed to:
Since they get routed via an MVC area, your views should be placed in the following folder:
~/App_Plugins/{areaname}/Views/{controllername}/
~/App_Plugins/{areaname}/Views/Shared/
Since you're only able to place static filese within your package's App_Plugin
folder, it's highly recommend to ensure that the area you use is the same as your package name, since that allows your views to be found.
The controller itself should not be placed in the App_Plugins folder, the App_Plugins folder is for static files only, compiled files like the controller will be included in the dlls used by the nuget package.
If you only want a surface controller action to be available when it's used within an Umbraco form and not from the auto-routed URL, you can add the [ValidateUmbracoFormRouteString]
attribute to the action method. This can be especially useful for plugin based controllers, as this makes sure the actions can only be activated from a form whenever it's used within the website.
Whenever you render an Umbraco form within your view using Html.BeginUmbracoForm<MyController>(...)
, the forms action will be the URL of the current page (not the auto-routed URL of the surface controller). Umbraco will therefore add a hidden ufprt
field to the form with an encrypted value containing the controller, action and optional area (known as the 'Umbraco form route string'). On form submission, this value is decrypted and Umbraco will activate the specified action of the surface controller.
In Umbraco 9 the __RequestVerificationToken
token is automatically added to forms for you, so you no longer need to add @Html.AntiForgeryToken()
to your forms.
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they are currently authenticated.
By default, Html.BeginUmbracoForm
and Html.BeginForm
adds an antiforgery token.
If the token is not added automatically, for instance, if you don't use Html.BeginUmbracoForm
or use an overload to Html.BeginForm
where you've set the antiForgery
parameter to false, you can add it manually like so:
If you are using a SurfaceController the antiforgery token will automatically be validated. However, if you are using a standard (non-umbraco) controller, you can manually specify it with the ValidateAntiForgeryToken
attribute:
The BeginUmbracoForm
and BeginForm
will only add the antiforgery token to the form as a hidden input. This means that you have to manually handle this if you're sending the request via JavaScript, for example, ajax.
The routing expects the antiforgery token to be in a header called RequestVerificationToken
. You can use the beforeSend
hook to read the antiforgery token and set it as a header if you're using ajax:
What the Umbraco Request Pipeline is
This section describes what the Umbraco Request Pipeline is. It explains how Umbraco matches a document to a given request and how it generates a URL for a document.
The request pipeline is the process of building up the URL for a node and resolving a request to a specified node. It ensures that the right content is sent back.
The pipeline works bidirectional: inbound and outbound.
Outbound is the process of building up a URL for a requested node. Inbound is every request received by the web server and handled by Umbraco.
This section will describe the components that you can use to modify Umbraco's request pipeline: IContentFinder & IUrlProvider
How the Umbraco inbound request pipeline works
The inbound process is triggered by UmbracoRouteValueTransformer
and then handled with the Published router. The process kicks in and creates a PublishedRequestBuilder
which will be used to create a PublishedContentRequest
.
The PublishedContentRequest
object represents the request which Umbraco must handle. It contains everything that will be needed to render it. All this occurs when the Umbraco modules knows that an incoming request maps to a document that can be rendered.
There are 3 important properties, which contains all the information to find a node:
Domain is a DomainAndUri object that is a standard Domain plus the fully qualified uri. For example, the Domain may contain "example.com" whereas the Uri will be fully qualified for example "https://example.com/".
It contains the content to render:
Contains template information:
The published request is created using the PublishedRequestBuilder
, which implements IPublishedRequestBuilder
. It's only in this builder that it's possible to set values, such as domain, culture, published content, redirects, and so on.
You can subscribe to the 'routing request' notification, which is published right after the PublishedRequestBuilder
has been prepared, but before the request is built, and processed. Here you can modify anything in the request before it is built and processed! For example content, template, etc:
How Umbraco prepares content requests
Is started in UmbracoRouteValueTransformer
where it gets the HttpContext
and RouteValueDictionary
from the netcore framework:
What it does:
It ensures Umbraco is ready, and the request is a document request.
Ensures there's content in the published cache, if there isn't it routes to the RenderNoContentController
which displays the no content page you see when running a fresh install.
Creates a published request builder.
Routes the request with the request builder using the PublishedRouter.RouteRequestAsync(…)
.
This will handle redirects, find domain, template, published content and so on.
Build the final IPublishedRequest
.
Sets the routed request in the Umbraco context, so it will be available to the controller.
Create the route values with the UmbracoRouteValuesFactory
.
This is what actually routes your request to the correct controller and action, and allows you to hijack routes.
Set the route values to the http context.
Handles posted form data.
Returns the route values to netcore so it routes your request correctly.
When the RouteRequestAsync
method is invoked on the PublishedRouter
it will:
FindDomain().
Handle redirects.
Set culture.
Find the published content.
Only if it doesn't exist, allowing you to handle it in a custom way with a custom router handler.
Find the template.
Set the culture (again, in case it was changed).
Publish RoutingRequestNotification
.
Handle redirects and missing content.
Initialize a few internal stuff.
We will discuss a few of these steps below.
The FindDomain method looks for a domain matching the request Uri
Using a greedy match: “domain.com/foo” takes over “domain.com”.
Sets published content request’s domain.
If a domain was found.
Sets published content request’s culture accordingly.
Computes domain Uri based upon the current request ("domain.com" for "http://domain.com" or "https://domain.com").
Else.
Sets published content request’s culture by default (first language, else system).
When finding published content the PublishedRouter
will first check if the PublishedRequestBuilder
already has content, if it doesn't the content finders will kick in. There a many different types of content finders, such as find by url, by id path, and more. If none of the content finders manages to find any content, the request will be set as 404, and the ContentLastChanceFinder
will run, this will try to find a page to handle a 404, if it can't find one, the ugly 404 will be used.
The PublishedRouter
will also follow any internal redirects there might be, it is however limited, as to not spiral out of control if there is an infite loop of redirects.
Once the content has been found, the PublishedRouter
moves on to finding the template.
First off it checks if any content was found, if it wasn't it sets the template to null, since there can't be a template without content.
Next it checks to see if there is an alternative template which should be used. An alternative template will be used if the router can find a value with the key "altTemplate", in either the querystring, form, or cookie, and there is content found by the contentfinders, so not the 404 page, or it's an internal redirect and the web routing setting has InternalRedirectPreservesTemplate
.
If no alternative template is found the router will get the template with the file service, using the ID specified on the published content, and then assign the template to the request.
If an alternative template is specified, the router will check if it's an allowed template for the content, if the template is not allowed on that specific piece of content it will revert to using the default template. If the template is allowed it will then use the file service to get the specified alternative template and assign the template to the request.
The router will pick up the redirect and redirect. There is no need to write your own redirects:
In case the router can't find a template, it will try and verify if there's route hijacking in place, if there is, it will run the hijacked route. If route hijacking is not in place, the router will set the content to null, and run through the routing of the request again, in order for the last chance finder to find a 404.
Information about Surface Controller Actions Result Helpers in Umbraco
A surface controller can return a few Umbraco specific actions.
Returns the current Umbraco page.
Redirects to the currently rendered Umbraco page.
This action can also take in a QueryString
object to be included in the redirect.
Redirects to the currently rendered Umbraco url.
Redirects to a given Umbraco page.
You can also redirect to a page key (GUID).
There are overloads for adding a QueryString
object.
A guide to implenting WebApi in Umbraco projects
This section will describe how to work with Web API in Umbraco to create REST services
Related links:
The Microsoft Web API reference can be found on the .
"ASP.NET enables you to build services that reach a broad range of clients, including browsers and mobile devices. With ASP.NET you use the same framework and patterns to build both web pages and services, side-by-side in the same project."
A great resource for getting started with creating web API's using .Net Core is the .
We have created a base API controller for developers to inherit from. This will ensure that the API controller gets routed. This does not expose any specific Umbraco-related services or objects but does inherit from the .Net Core controller base. This means that you will have access to the same things you would from a regular .Net Core controller. Dependency injection is also available to controllers. Any Umbraco-specific services or objects you might need can be injected into the constructor.
The class to inherit from is: Umbraco.Cms.Web.Common.Controllers.UmbracoApiController
There are 2 types of Umbraco API controllers:
A locally declared controller - is not routed via an Area.
A plugin based controller - is routed via an Area.
When working on your own projects you will normally be creating a locally declared controller which requires no additional steps. However, if you are creating an Umbraco package, to be distributed, you will want to create a plugin based controller so it gets routed via its own area. This ensures that the route will not overlap with someone's locally declared controller if they are both named the same thing.
It is very important that you name your controllers according to these guidelines or else they will not get routed:
All controller class names must be suffixed with "Controller" and inherit from UmbracoApiController. Some examples:
Example:
All locally declared Umbraco API controllers will be routed under the url path of:
~/Umbraco/Api/[YourControllerName]
E.g. *~/Umbraco/Api/Products/GetAllProducts
Note that the "Controller" part of your controller name gets stripped away.
If you are creating an Umbraco API controller to be shipped in an Umbraco package you will need to add the Umbraco.Cms.Web.Common.Attributes.PluginController
attribute to your controller to ensure that it is routed via an area. The area name is up to you to specify in the attribute.
Example:
Now this controller will be routed via the area called "AwesomeProducts". All plugin based Umbraco API controllers will be routed under the url path of:
~/Umbraco/[YourAreaName]/[YourControllerName]
E.g. ~/Umbraco/AwesomeProducts/Products/GetAllProducts
If you are creating a controller to work within the Umbraco backoffice then you will need to ensure that it is secured properly by inheriting from: UmbracoAuthorizedApiController
or UmbracoAuthorizedJsonController
. This controller type will auto-route your controller like the above examples except that it will add another segment to the path: 'backoffice'.
~/Umbraco/backoffice/Api/[YourControllerName]
~/Umbraco/backoffice/[YourAreaName]/[YourControllerName]
E.g. ~/Umbraco/backoffice/Api/Products/GetAllProducts
or
~/Umbraco/backoffice/AwesomeProducts/Products/GetAllProducts
for PluginController
Attribute routing uses attributes to define routes. Attribute routing gives you more control over the URIs in your web application.
To exclude any endpoint or folders in your directory from Umbraco's routing, add it to the ReservedPaths
setting in the appsettings.json
file.
For example:
To use attribute routing, add the Microsoft.AspNetCore.Mvc.Route
attribute to the controller or controller action you want to route. If you want to attribute route an entire controller you have to add the [action]
token in order to route to an action, for instance:
This route the controllers actions like so:
~/products/GetAllProducts
and ~/products/GetProduct
If you use the route attribute for a specific action the [action]
token is not nececary, but you can request parameters from the path in a similar manner, using the {parameterName}
syntax, for instance:
Here the GetAllProducts
endpoint will be routed normally, but the GetProduct
will be routed as ~/product
where you can optionally access it as ~/product/4
, or any other number, if a number is included as the last segment of the path, let's say 4, the action will return "Monitor model 4", otherwise it will just return "Base model Monitor".
The outbound pipeline consists out of the following steps:
To explain things we will use the following content tree:
In our example "Our Products" will become "our-products" and "Swibble" will become "swibble".
The segments are created by the "Url Segment provider"
The DI container of an Umbraco implementation contains a collection of UrlSegmentProviders
. This collection is populated during Umbraco boot up. Umbraco ships with a 'DefaultUrlSegmentProvider' - but custom implementations can be added to the collection.
When the GetUrlSegment
extension method is called for a content item + culture combination, each registered IUrlSegmentProvider
in the collection is executed in 'collection order'. This continues until a particular UrlSegmentProvider
returns a segment value for the content, and no further UrlSegmentProviders
in the collection will be executed. If no segment is returned by any provider in the collection a DefaultUrlSegmentProvider
will be used to create a segment. This ensures that a segment is always created, like when a default provider is removed from a collection without a new one being added.
To create a new Url Segment Provider, implement the following interface:
Note each 'culture' variation can have a different Url Segment!
The returned string will be the Url Segment for this node. Any string value can be returned here but it cannot contain the URL segment separator character /
. This would create additional "segments" - something like 5678/swibble
is not allowed.
For the segment of a 'product page', add its unique SKU / product ref to the existing Url segment:
The returned string becomes the native Url segment - there is no need for any Url rewriting.
For our "swibble" product in our example content tree, the ProductPageUrlSegmentProvider
would return a segment "swibble--123xyz". In this case, 123xyz is the unique product sku/reference for the swibble product.
Register the custom UrlSegmentProvider with Umbraco, either using a composer or an extension method on the IUmbracoBuilder
:
The Default Url Segment provider builds its segments by looking for one of the below values, checked in this order:
A property with alias umbracoUrlName on the node. (this is a convention led way of giving editors control of the segment name - with variants - this can vary by culture).
The 'name' of the content item e.g. content.Name
.
The Umbraco string extension ToUrlSegment()
is used to produce a clean 'Url safe' segment.
To create a path, the pipeline will use the segments of each node to produce a path.
If we look at our example, the "swibble" node will receive the path: "/our-products/swibble". If we take the ProductPageUrlSegmentProvider
from above, the path would become: "/our-products/swibble-123xyz".
But, what if there are multiple websites in a single Umbraco Implementation? in this multi-site scenario then an (internal) path to a node such as "/our-products/swibble-123xyz" could belong to any of the sites, or match multiple nodes in multiple sites. In this scenario additional sites will have their internal path prefixed by the node id of their root node. Any content node with a hostname defines a “new root” for paths.
Paths can be cached, what comes next cannot (http vs https, current request…).
Domain without path e.g. "www.site.com" will become "1234/path/to/page"
Domain with path e.g. "www.site.com/dk" will produce "1234/dk/path/to/page" as path
No domain specified: "/path/to/page"
Unless HideTopLevelNodeFromPath config is true, then the path becomes "/to/page"
In our example the "swibble" node could have the following URL: "http://example.com/our-products/swibble"
Generating this url is handled by the Url Provider. The Url Provider is called whenever a request is made in code for a Url e.g.:
The DI container of an Umbraco implementation contains a collection of UrlProviders
this collection is populated during Umbraco boot up. Umbraco ships with a DefaultUrlProvider
- but custom implementations can be added to the collection. When .Url is called each IUrlProvider
registered in the collection is executed in 'collection order' until a particular IUrlProvider
returns a value. (and no further IUrlProviders
in the collection will be executed.)
Umbraco ships with a DefaultUrlProvider
, which provides the implementation for the out of the box mapping of the structure of the content tree to the url.
If the current domain matches a root domain of the target content.
Return a relative Url.
Else must return an absolute Url.
If the target content has only one root domain.
Use that domain to build the absolute Url.
If the target content has more than one root domain.
Figure out which one to use.
To build the absolute Url.
Complete the absolute Url with scheme (http vs https).
If the domain contains a scheme use it.
Else use the current request’s scheme.
If "addTrailingSlash" is true, then add a slash.
Then add the virtual directory.
If the URL provider encounters collisions when generating content URLs, it will always select the first available node and assign the URL to this one. The remaining nodes will be marked as colliding and will not have a URL generated. Fetching the URL of a node with a collision URL will result in an error string including the node ID (#err-1094) since this node does not currently have an active URL. This can happen if an umbracoUrlName property is being used to override the generated URL of a node, or in some cases when having multiple root nodes without hostnames assigned.
This means publishing an unpublished node with a conflicting URL, might change the active node being rendered on that specific URL in cases where the published node should now take priority according to sort order in the tree!
Create a custom Url Provider by implementing IUrlProvider
interface:
The url returned in the 'UrlInfo' object by GetUrl can be completely custom.
If implementing a custom Url Provider, consider following things:
Cache things.
Be sure to know how to handle schema's (http vs https) and hostnames.
Inbound might require rewriting.
If there is only a small change to the logic around Url generation, then a smart way to create a custom Url Provider is to inherit from the DefaultUrlProvider and override the GetUrl() virtual method.
Register the custom UrlProvider with Umbraco:
The GetOtherUrls method is only used in the Umbraco Backoffice to provide a list to editors of other Urls which also map to the node.
For example, let's consider a convention-led umbracoUrlAlias
property that enables editors to specify a comma delimited list of alternative urls for the node. It has a corresponding AliasUrlProvider
registered in the UrlProviderCollecton
to display this list to the Editor in the backoffice Info Content app for a node.
Specifies the type of urls that the url provider should produce, eg. absolute vs. relative Urls. Auto is the default
These are the different modes:
Default setting can be changed in the Umbraco:CMS:WebRouting section of appsettings.json
:
The ISiteDomainMapper
implementation is used in the IUrlProvider
and filters a list of DomainAndUri
to pick one that best matches the current request.
Create a custom SiteDomainMapper by implementing ISiteDomainMapper
The MapDomain methods will receive the Current Uri of the request, and custom logic can be implemented to decide upon the preferred domain to use for a site in the context of that request. The SiteDomainMapper's role is to get the current Uri and all eligible domains, and only return one domain which is then used by the UrlProvider to create the Url.
Only a single ISiteDomainMapper
can be registered with Umbraco.
Register the custom ISiteDomainMapper
with Umbraco using the SetSiteDomainHelper
extension method
Umbraco ships with a default SiteDomainMapper
. This has some useful functionality for grouping sets of domains together. With Umbraco Cloud, or another Umbraco development environment scenario, there maybe be multiple domains setup for a site 'live, 'staging', 'testing' or a seperate domain to access the backoffice. Each domain will be setup as a 'Culture and Hostname' inside Umbraco. By default editors will see the full list of possible Urls for each of their content items on each domain, which can be confusing. If the additional urls aren't present in Culture and Hostnames, then when testing the front-end of the site on a 'staging' url, will result in navigation links taking you to the registered domain!
What the editor sees without any SiteDomainMapper, visiting the backoffice url:
Which is 'noise' and can lead to confusion: accidentally clicking the staging url, which is likely to be served from a different environment / different database etc may display the wrong content...
To avoid this problem, use the default SiteDomainMapper's AddSite method to group Urls together.
Since the SiteDomainMapper is registered in the DI, we can't consume it directly from a composer, so first create a component which adds the sites in the initialize method:
Then add the component with a composer:
Now if an editor visits the backoffice via the staging url they will only see domains for the staging url:
Now if an editor visits the backoffice via the backoffice url they will only see domains for the backoffice url and the production url:
NB: it's not a 1-1 mapping, but a grouping. Multiple Urls can be added to a group. Think multilingual production and staging variations, and in the example above, if an editor logged in to the backoffice via the production url, eg umbraco-v8.localtest.me/umbraco - they would see the umbraco-v8-backoffice.localtest.me domain listed.
The SiteDomainMapper contains a 'BindSites' method that enables different site groupings to be bound together:
Visiting the backoffice now via umbraco-v8-backoffice.localtest.me/umbraco would list all the 'backoffice' grouped domains AND all the 'staging' grouped domains.
For more information, see the article.
You can read more about the surface controller .
You can also implement your own content finders and last chance finder, for more information, see
This is the most common way to create an Umbraco API controller, you inherit from the class Umbraco.Cms.Web.Common.Controllers.UmbracoApiController
and that is all. You will need to follow the guidelines specified by Microsoft for creating a Web API controller, documentation can be found on the .
For more information about areas, Urls and routing see the
For more information, see the article.
This is not anything Umbraco specific, so to read more about attribute routing, see the .
When the URL is constructed, Umbraco will convert every node in the tree into a segment. Each published item has a corresponding url segment.
Node | Segment | Internal Path |
---|
The Url of a node consists of a complete : the Schema, Domain name, (port) and the path.
Add /fish on the end of every url. It's important to note here that since we're changing the outbound url, but not how we handle urls inbound, this will break the routing. In order to make the routing work again you have to implement a custom content finder, see for more information on how to do that.
See for more information on routing settings.
Our Values | our-values | /our-values |
Our Products | our-products | /our-products |
Swibble | swibble-123xyz | /our-products/swibble-123xyz |
Dibble | dibble-456abc | /our-products/dibble-456abc |
Another Site | another-site | 9676/ |
Their Values | their-values | 9676/their-values |
How api controllers are routed and how to retrieve their URLs
This section will describe how Umbraco Api controllers are routed and how to retrieve their URLs
Like Surface Controllers in Umbraco, when you inherit from the base class Umbraco.Cms.Web.Common.Controllers.UmbracoApiController
we will auto-route this controller so you don't have to worry about routing at all.
All locally declared Umbraco api controllers will be routed under the url path of:
~/Umbraco/Api/[YourControllerName]
All plugin based Umbraco api controllers will be routed under the url path of:
~/Umbraco/[YourAreaName]/[YourControllerName]
We've added some handy UrlHelper
extension methods to help you retrieve the Url of your Umbraco Api controllers. The extension methods are found in the class: Umbraco.Extensions.UrlHelperExtensions
so you'll need to ensure you have the namespace Umbraco.Extensions
imported. You will also need to inject UmbracoApiControllerTypeCollection
, if you want to use any of the overloads that require it.
The method overloads are:
The most consistent way to retrieve a Url is to use your controller's type, and an expression to select the action. Example of retrieving a URL in a view:
Generally a UrlHelper instance will be available on most base classes like Controllers and Views, and you shouldn't have to create it manually, but if you need to you can, by injecting IUrlHelperFactory
and IActionContextAccessor
and then use the factory like so:
How to secure your Umbraco Api controllers
This section will describe how to secure your Umbraco Api controllers based on a users membership
Probably the easiest way to ensure your controller is secured for only backoffice users is to inherit from Umbraco.Cms.Web.BackOffice.Controllers.UmbracoAuthorizedApiController
. This is essentially the same as applying [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
to your controller (see below).
The UmbracoAuthorizedApiController
is automatically routed. Check out the routing documentation for more information on this topic.
To secure your controller based on backoffice membership use the attribute: Microsoft.AspNetCore.Authorization.Authorize
, with the policy parameter set to AuthorizationPolicies.BackOfficeAccess
, like so: [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
.
This attribute will ensure that a valid backoffice user is logged in, however it's very important to note that this only works if the controller is routed to /umbraco/backoffice/*
.
Examples:
This will only allow a logged in backoffice user to access the GetAllProducts action:
To secure your controller based on front-end membership use the attribute: Umbraco.Cms.Web.Common.Filters.UmbracoMemberAuthorize
.
There are 3 parameters that can be supplied to control how the authorization works:
To allow all members, use the attribute without supplying any parameters.
You can apply these attributes at the controller level or at the action level.
Examples:
This will only allow logged in members of type "Retailers" to access the GetAllProducts action:
This will only allow member's belonging to the group VIP to access any actions on the controller:
This will only allow member's with Ids 1, 10 and 20 to access any actions on the controller: