Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
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 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.
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 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 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.
Is your custom route endpoint considered a client-side request?
Learn more about how to handle these in the Client-Side Requests section of this article.
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 Collection, 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.
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!
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 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
Customizing the ASP.NET middleware pipeline in Umbraco
Middleware is responsible for processing/customizing an incoming request before generating an outgoing response. Shortly defined the middleware is a step to check the requests before giving a response back.
Umbraco automatically configures all required middleware in the WithMiddleware()
method in a specific order based on the Microsoft documentation.
You can use Umbraco pipeline filters in case you want to add your own middleware before, in-between or after the default Umbraco middleware. Filters are added by configuring the UmbracoPipelineOptions
and require an instance of IUmbracoPipelineFilter
that contains the following callbacks:
PrePipeline
- executed before any Umbraco-specific middleware is added, an example can be URL rewrites.
PreRouting
- executed after the static files middleware and before the routing middleware is added (using UseRouting()
). It can also be used to change the incoming URL.
PostRouting
- executed after the routing middleware is added and can be used to configure Cross-origin resource sharing (CORS).
PostPipeline
- executed after all Umbraco-specific middleware is added.
Endpoints
- executed right before the Umbraco-specific endpoints are added using WithEndpoints()
.
The addition of the PostRouting
callback is to allow correctly configuring the Cross-Origin Resource Sharing (CORS) middleware without having to use the WithCustomMiddleware()
method.
IUmbracoPipelineFilter
is an interface in Umbraco that allows the creation of custom filters which then modifies the behavior of the request pipeline. It can be used to change different aspects of how Umbraco handles incoming requests, such as changing content or adding security checks.
WithCustomMiddleware()
is a method that can be used in Umbraco for adding custom middleware. This includes some specific customizable instructions that run in the request processing pipeline.
Using WithCustomMiddleware()
instead of WithMiddelware()
should only be used as a last resort. This is because Umbraco can break if you forget to add middleware or add them in the wrong order.
Create a composer with the following:
You should be able to request /echo
from any origin, but get an error when trying to fetch /echo2
from a foreign origin. This can be tested by pasting the following JavaScript code in browser console when not on the local origin/Umbraco website (such as umbraco.com):
This should return the following:
An additional bonus is that this doesn't require any changes in your Program.cs
file. It also allows packages to enable Cross-Origin Resource Sharing (CORS) and configure their own policies.
Users that currently use WithCustomMiddleware()
will need to add calls to RunPreRouting()
and RunPostRouting()
. This is similar to Umbraco adding additional middleware in future versions.
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
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 behavior 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:
For example:
To wire up a concrete instance of IMadeUpProductService, use a composer:
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 Program.cs
.
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 Program.cs
class.
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 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 .
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:
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 can use either an extension on the Umbraco builder or a composer to access the ContentFinderCollection
and add or remove specific ContentFinders
.
First create the extension method:
Then invoke in the Program.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 Program.cs
file:
When adding a custom IContentLastChanceFinder
to the pipeline any Error404Collection
-settings in appSettings.json
will be ignored.
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.
Umbraco HQ offers a full-day training course covering advanced routing, custom page controllers and models, and backoffice integration. The course targets backend ASP.NET MVC developers working with service and application integration on Umbraco.
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.
Injecting services into your controller constructors is possible with Umbraco's underlying dependency injection implementation. See for more info on this.
See for further information.
See for more configuration options
Learn more about registering dependencies and when to use which method in the article.
to learn more about the topics covered and how it can enhance your Umbraco development skills.
The pipeline works bidirectional: and .
is the process of building up a URL for a requested node. 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: & IUrlProvider
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.
You can also implement your own content finders and last chance finder, for more information, see IContentFinder
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.
How the Umbraco inbound request pipeline works
The inbound process is triggered by UmbracoRouteValueTransformer
and then handled with the Published router. The published content request preparation 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:
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:
For more information, see the Antiforgery in ASP.NET Core article.
Surface controller actions can be asynchronous. A common naming convention for asynchronous methods is using an Async
suffix for the action name. However, this will not work by default due to the inner workings of ASP.NET Core MVC.
Consider the following asynchronous surface controller action:
To use this action in a view you can add this:
But once you click the button, you will encounter an error message along the lines of: InvalidOperationException: Could not find a Surface controller route in the RouteTable for controller name MySurface
.
To counter this you need to instruct ASP.NET Core MVC to explicitly accept the Async
suffix for controller names in the program.cs
:
Alternatively you can rename your surface controller action so it does not contain the Async
suffix.
You can read more about the surface controller action result helpers.
A guide to implementing APIs in Umbraco projects
This article describes how to work with API controllers in Umbraco projects. It focuses on creating REST services using ASP.NET Core-based API controllers.
In Umbraco 13 and below, the recommended approach was to base API controllers on the UmbracoApiController
class. However, UmbracoApiController
is obsolete in Umbraco 14 and will be removed in Umbraco 15.
Read the article Porting old Umbraco APIs for more details.
To better understand the basics of APIs, you can see the Microsoft ASP.NET Core API documentation. The documentation provides a solid foundation for API concepts in .NET environments..
Public APIs in Umbraco are similar to standard ASP.NET Core APIs. Below is an example of how to create an API in Umbraco:
You can secure your public APIs using front-end membership protection with the [UmbracoMemberAuthorize]
attribute. This attribute allows you to restrict access based on member types, groups, or specific member IDs.
The available parameters are:
AllowType
: A comma-delimited list of allowed member types.
AllowGroup
: A comma-delimited list of allowed member groups.
AllowMembers
: A comma-delimited list of allowed member IDs.
To allow all members, apply the [UmbracoMemberAuthorize]
attribute without parameters.
You can apply these attributes either at the controller level or at the action level.
Read more about members and member login in the Member Registration and Login article.
The [UmbracoMemberAuthorize]
attribute offers flexible options for securing your public APIs in Umbraco. The following examples show different ways to apply member protection, such as how to restrict access by member type, group, or specific IDs.
In this example, any logged in member can access all actions in the ProductsController
controller:
This example allows only logged-in members of type "Retailers" to access the GetAll
action:
In this example, only members belonging to the "VIP" group can access any actions on the controller:
This example allows only members with IDs 1, 10, and 20
to access the GetAll
action:
Umbraco's Backoffice API is also known as the Management API. When you create API controllers for Umbraco's backoffice, you are writing Management API controllers.
For a detailed guide on how to create APIs for the Backoffice, see the Creating a Backoffice API article article.
Tips to porting over API controllers from Umbraco 13 and below
Umbraco 14 has obsoleted or removed base classes that were widely adopted for building APIs in previous versions of Umbraco. This article outlines the recommended approach for porting over APIs that were created before Umbraco 14.
UmbracoApiController
implementationsUmbracoApiController
is obsolete from Umbraco 14 and will be removed in Umbraco 15. The recommended approach is to base APIs on the ASP.NET Core Controller
class instead.
UmbracoApiController
would automatically route the API actions to /umbraco/api/[ControllerName]/[ControllerAction]
. Moving forward, you control your API routes with the [Route]
annotation.
If you rely on the route previously generated by UmbracoApiController
, you can still create identical routes. For example, the following Controller
implementation:
...is routing-wise equivalent to this UmbracoApiController
implementation:
In both cases, the API endpoint will be available at /umbraco/api/products/getall
.
In previous versions of Umbraco, it was possible to route controllers to dedicated areas by annotating UmbracoApiController
implementations with [PluginController]
. This annotation would automatically route the API actions to /Umbraco/[PluginAreaName]/[ControllerName]/[ActionName]
.
As this approach was based on UmbracoApiController
, it is no longer viable - use the [Route]
annotation instead. For example, the following Controller
implementation:
...is routing wise equivalent to this [PluginController]
annotated implementation:
In both cases, the API endpoint will be available at /umbraco/shop/products/getall
.
The UmbracoAuthorizedApiController
and UmbracoAuthorizedJsonController
base classes have been removed in Umbraco 14. Moving forward, backoffice APIs (also known as Management APIs) must be based on the ManagementApiControllerBase
.
Read the Creating a Backoffice API article for a comprehensive guide to writing APIs for the Management API.
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".
Since the release of Umbraco v9 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 v9+ 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 v9+ you have to register the middleware in your Program.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 Program.cs
file) containing:
In the Program.cs
file you can add the URL Rewriting Middleware 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.
How the Umbraco outbound request pipeline works
The outbound pipeline consists out of the following steps:
To explain things we will use the following content tree:
When the URL is constructed, Umbraco will convert every node in the tree into a segment. Each published Content item has a corresponding URL segment.
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"
The Url of a node consists of a complete URI: the Schema, Domain name, (port) and the path.
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 the 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 the 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.
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 IContentFinder for more information on how to do that.
The below example is using ILocalizationService
which is currently obselete and will be removed in v15. Use ILanguageService
or IDictionaryItemService
(for dictionary item operations) instead.
Register the custom UrlProvider with Umbraco:
If you want to have multiple URL providers, you can add them one after the other with multiple Insert
methods. Umbraco will cycle through all the providers registered until it finds one that doesn't return null
. If all custom URL providers return null
it will fall back to the default URL provider. The last added with Insert
is the first that will be executed.
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
:
See WebRouting config reference documentation for more information on routing settings.
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 separate 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.
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.
Node | Segment | Internal Path |
---|---|---|
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