Part I: An Introduction to ASP.NET StatusCodePages

By Sam Steele On October 20, 2021

Part I: An Introduction to ASP.NET StatusCodePages

Error Pages in Kentico Xperience

In almost every MVC site I've developed on the Kentico Xperience Platform, there has been one feature that has never quite felt "right": Error Pages.

Whether it's due to confusing configuration (looking at you, System.Web), awkward routing and redirects in controllers, or multiple, sometimes ambiguous, patterns for handling exceptions (looking at you again, System.Web), creating user-friendly error pages in Kentico Xperience MVC sites doesn't always seem straight-forward.

It's time for a reckoning.

In this 3-part series, we'll be covering the background to BizStream's xperience-status-code-pages packages, reviewing how the ASP.NET Core StatusCodePages pattern can be extended to serve user-friendly error pages from the Kentico Xperience Content Tree.

An Introduction to ASP.NET Core StatusCodePages

Within ASP.NET Core framework, StatusCodePages is a pattern for providing a response body, based on the response's status code.

The StatusCodePages pattern provides a variety of IApplicationBuilder extension methods that configure the behavior of the underlying middleware, and allow developers some options as to how the response should be handled. Of these extension methods, UseStatusCodePagesWithReExecute is particularly attractive, as it allows developers to specify a path upon which to re-execute the request pipeline.

An advantage to "ReExecute" over "Redirect", is that the original URL is preserved in the client. A request to /not-a-real-url that produces a 404: Not Found status code is returned to the requested URL, /not-a-real-url, rather than redirecting the client to /error?code=404. If you're a veteran ASP.NET developer with IIS Rewrite Module experience: a "ReExecute" is similar to a "Rewrite", as opposed to a "Redirect"

For Example:

public class Startup
{
  // ...

  public void Configure( IApplicationBuilder app )
  {
    app.UseStatusCodePagesWithReExecute( "/error", "?code={0}" );

    // ...
  }
}
public class ErrorController : Controller
{

  [HttpGet( "error" )]
  public IActionResult Error( int code )
    => Content( $"Http Error: {code}" );

}
GET https://localhost:5001/not-a-real-url Http Error: 404

An additional advantage to this pattern, is that in other controllers, the base Controller.NotFound method can be used, as opposed to redirecting to a custom "NotFound" action:

public class ProductController : Controller
{

  [HttpGet( "products/{slug}" )]
  public IActionResult Index(
    [FromServices] IProductService productService,
    string slug
  )
  {
    var product = productService.GetProductBySlug( slug );

    // NO MORE YUCK!
    // if( product is null )
    // {
    //   return RedirectToAction( "NotFound", "Error" );
    // }

    // Yay!
    if( product is null )
    {
      return NotFound();
    }

    return View( product );
  }

}

The RedirectToAction( "NotFound", "Error" ) is a common pattern I often see in Kentico Xperience MVC sites, wherein the NotFound action queries the Kentico Xperience Content Tree for some type of NotFoundNode.

Serve StatusCodePages from Kentico Xperience

In many cases, we'd like to give Content Editors the ability to manage the content of Error Pages. This can be achieved by building upon the ErrorController from the previous example to query and serve content from the Kentico Xperience Content Tree:

public class ErrorController : Controller
{

  [HttpGet( "error" )]
  public async Task Error( [FromServices] IPageRetriever pageRetriever, int code )
  {
    var page = (
      await pageRetriever.RetrieveAsync(
        nodes => nodes.WhereEquals( nameof( HttpErrorNode.HttpStatusCode ), code )
          .Or( condition => condition.WhereEquals( nameof( HttpErrorNode.HttpStatusCode ), 500 ) ),
          .TopN( 1 )
        cache => cache.Dependencies( (_, __) => { } )
          .Key( $"httperrornode|{code}" ),
        HttpContext.RequestAborted
      )
    )?.FirstOrDefault();

    var viewModel = new ErrorViewModel
    {
      Content = page?.Content ?? "There was an error processing the request.",
      Heading = page?.Heading ?? "Internal Server Error"
    }

    return View( viewModel );
  }

}

That's quite a bit a query logic in the controller...

We can clean that up by defining a service to compose the IPageRetriever usage and query logic. We'll try to work with Kentico's "Page Retrievers" pattern here by defining an IErrorPageRetriever that describes the retrieval of an HttpErrorNode:

public interface IErrorPageRetriever
{

  Task RetrieveAsync( int code, CancellationToken cancellation = default );

}
Refactoring the code from the ErrorController, the ErrorPageRetriever is as follows:
public class ErrorPageRetriever : IErrorPageRetriever
{
  #region Fields
  private readonly IPageRetriever pageRetriever;
  #endregion

  public ErrorPageRetriever( IPageRetriever pageRetriever )
    => this.pageRetriever => pageRetriever;

  public async Task RetrieveAsync( int code, CancellationToken cancellation = default )
    => (
      await pageRetriever.RetrieveAsync(
        nodes => nodes.WhereEquals( nameof( HttpErrorNode.HttpStatusCode ), code )
          .Or( condition => condition.WhereEquals( nameof( HttpErrorNode.HttpStatusCode ), 500 ) ),
          .TopN( 1 )
        cache => cache.Dependencies( (_, __) => { } )
          .Key( $"httperrornode|{code}" ),
        cancellation
      )
    )?.FirstOrDefault();

}
_The "no-op" (.Dependencies( (_, __) => { } )) call to IPageCacheBuilder.Dependencies( Action, IPageCacheDependencyBuilder> configureDependenciesAction, bool includeDefault = true )is required to ensure theHttpErrorNoderetrieved is cached using the defaults, via theincludeDefault flag._ 

Add our services to the IServiceCollection in the Startup, and the ErrorController is a bit more concise.

public class Startup
{

  public void ConfigureServices( IServiceCollection services )
  {
    // ...

    services.AddTransient();

    // ...
  }

}
public class ErrorController : Controller
{

  [HttpGet( "error" )]
  public async Task Error( [FromServices] IErrorPageRetriever errorPageRetriever, int code )
  {
    var page = await errorPageRetriever.RetrieveAsync( code, HttpContext.RequestAborted );
    var viewModel = new ErrorViewModel
    {
      Content = page?.Content ?? "There was an error processing the request.",
      Heading = page?.Heading ?? "Internal Server Error"
    }

    return View( viewModel );
  }

} 

One Small Problem...

All of this is nice, except for one. little. thing. /error?code={0}. All of the error pages are accessed at the /error path with a query string, Yuck! An ideal solution would allow us to use Page Routing to give Content Editors the ability to configure the URLs of the error pages, just like any other node in the Content Tree. Page Routing would also give greater flexibility to support Redirect or ReExecute behaviors.

We are now presented with a new problem: UseStatusCodePagesWithReExecute and UseStatusCodePagesWithRedirects require a specific path string that must be known when Startup.Configure(IApplicationBuilder) is invoked and the UseStatusCodePages* method is called. In order to make Page Routing work, the StatusCodePages middleware requires a means of dynamically querying the Content Tree for the PageUrl of the expected HttpErrorNode in which the request should Redirect to or ReExecute on.

Unfortunately, neither of the WithRedirects or WithReExecute extensions provide overloads that would allow dynamic retrieval of the path to Redirect to or ReExecute on. There is still hope, however, thanks to the UseStatusCodePages(IApplicationBuilder, Func<StatusCodeContext,Task>) overload. This overload allows a Func<StatusCodeContext, Task> delegate to be passed, which effectively functions as a Request Delegate via the StatusCodeContext.

In Part II: Implementing Kentico Xperience StatusCodePages Middleware, we'll explore how UseStatusCodePages(IApplicationBuilder, Func<StatusCodeContext,Task>) can be used to implement our own WithRedirects/WithReExecute extension methods that query the Kentico Xperience Content Tree for the PageUrl to Redirect to or ReExecute on, in a manner that is compatible with the behavior of the extensions provided by the framework.

If you are looking for more help with your Kentico Xperience projects, feel free to reach out to BizStream directly.

Click here to read more Kentico posts
Start a Project with Us
Photo of the author, Sam Steele

About the author

Sam loves learning. While teaching himself to program, Sam found a place of friends in the Open Source Community where he was able to grow his skills with other young developers. His primary goal at BizStream is to learn and gain as much experience from the rest of the team. When he's not programming, you can find Sam playing his favorite video game, Rainbow Six Siege, watching movies old movies, or tinkering with his car.

View other posts by Sam

Subscribe to Updates

Stay up to date on what BizStream is doing and keep in the loop on the latest with Kentico.