Part I: An Introduction to ASP.NET StatusCodePages

The first part in a three part series about the background to BizStream's xperience-status-code-pages packages.

Errror 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 don’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 the 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 StatsCodePages 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 of 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.

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 2

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.

Subscribe to Our Blog

Stay up to date on what BizStream is doing and keep in the loop on the latest in marketing & technology.