Sitecore Rendering Engine routing explained

If we want to be able to fully understand the way routing works in Sitecore’s ASP.NET Core Rendering Engine, we first need to understand how the out-of-the-box routing works in ASP.NET Core.

ASP.NET Core routing

ASP.NET Core uses a concept called “Middleware” to compose a request handling pipeline. This pipeline can be composed of multiple middleware components where each component performs operations on the HttpContext object and either terminates the request (when returning a result to the client) or triggers the next middleware component.

ASP.NET Core Middleware

These middleware components are added in the Configure method in the Startup.cs of your ASP.NET Core project.
A basic MVC ASP.NET Core middleware configuration would look something like this:

public void Configure(IApplicationBuilder app)
{
    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
        endpoints.MapRazorPages();
    });
}

Using this configuration each request would follow a pipeline process like:

  1. UseHttpsRedirection; Redirect HTTP to HTTPS

  2. UseStaticFiles; If the request path corresponds to a file on disk, return that file.

  3. UseRouting; Enable the usage of endpoints configuration

    1. MapDefaultControllerRoute; Check if controller and action exists corresponding with a default MVC route like /{controller}/{action}/{id?} and execute it
    2. MapRazorPages; Return a Razor Page result if any correspond to the path

Each of these middleware components can terminate the pipeline, so if a static file exists corresponding to the request path it won’t trigger the components after that.

The Sitecore way

Now that we know what comes out-of-the-box with ASP.NET Core, what is it that Sitecore built on top of this?
The Sitecore Rendering Engine inserts its own middleware components, but doesn’t actually use such a component to handle routing directly. Instead they assign a ‘fallback’ controller, resulting in all requests being processed by the same controller and action.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        "error",
        "error",
        new { controller = "Default", action = "Error" }
    );

    endpoints.MapControllerRoute(
        "healthz",
        "healthz",
        new { controller = "Default", action = "Healthz" }
    );

    // Enables the default Sitecore URL pattern with a language prefix.
    endpoints.MapSitecoreLocalizedRoute("sitecore", "Index", "Default");

    // Fall back to language-less routing as well, and use the default culture (en).
    endpoints.MapFallbackToController("Index", "Default");
});

A DefaultController with method Index is created in the web project for this fallback purpose.

[UseSitecoreRendering]
public IActionResult Index(Route route)
{
	...

	return View(route);
}

This Index method has two key characteristic. First it has a parameter of object type Route and second it has a data attribute UseSitecoreRendering. Populating this Route parameter is a two step process:

As a first step, the UseSitecoreRendering attribute adds another middleware component to the request configuration. This component then triggers a request to the Sitecore Layout Service to fetch all the page and rendering data for the context page. This Layout Service request is performed by an ISitecoreLayoutClient service, which is configured in the ConfigureServices method of the Startup.cs:

// Register the Sitecore Layout Service Client, which will be invoked by the Sitecore Rendering Engine.
services.AddSitecoreLayoutService()
    // Set default parameters for the Layout Service Client from our bound configuration object.
    .WithDefaultRequestOptions(request =>
    {
        request
            .SiteName(Configuration.DefaultSiteName)
            .ApiKey(Configuration.ApiKey);
    })
    .AddHttpHandler("default", Configuration.LayoutServiceUri)
    .AsDefaultHandler();

What is important to note here, is that this is the place the site name and API key for the requests are set. These settings are added to the request query string for the Layout Service to determine the context site of the request and to check if the application actually has the authorization to perform this request based on the API key.

Once the middleware component has received the response, it creates a SitecoreRenderingContext object including the Layout Service response and stores it in the HttpContext.

The second step then uses this SitecoreRenderingContext object. Sitecore has added a BindingSource called SitecoreLayoutRouteBindingSource, which then takes the Route object from the SitecoreRenderingContext in the HttpContext and returns is. Through model binding this Route object is that the source for populating the Route object parameter of the Index method.

In action

So let’s see this in action. Taking the MVP-Site example setup (https://github.com/Sitecore/MVP-Site), when I open the About page of the MVP site I get to see the following page.

About page of MVP site

During the request processing in the Rendering Engine, the ISitecoreLayoutClient would make a GET request using the following URL:
https://mvp-cd.sc.localhost/sitecore/api/layout/render/jss?item=/About&sc_lang=en&sc_apikey={E2F3D43E-B1FD-495E-B4B1-84579892422A}&sc_site=mvp-site

Note that the URL contains the request path in the query string as “item=/About”, just like it contains the language, API key and site name.

If we were to open this URL in directly in a browser, it would show us the raw Layout Service response.

Layout Service JSON result of about page

Based on this JSON response the Rendering Engine then renders the entire page.

Let’s take a closer look at how this rendering process works in a next blog post!