Part II: Implementing Kentico Xperience StatusCodePages Middleware

Part Two in this three-part series about StatusCodePages. In this post, you will learn about implementing Kentico Xperience StatusCodePages Middleware.

Error Pages in Kentico Xperience

InĀ Part I: An Introduction toĀ ASP.NET CoreĀ StatusCodePages, we explored ASP.NET Core StatusCodePages and how we can use this middleware provided by the framework to produce user-friendlyĀ error pages from the Kentico Xperience Content Tree using a customĀ Controller. In this predecessor toĀ Part I, we’ll dip our toes into the waters of the ASP.NET Core StatusCodePages source to implement a customĀ Request Delegate Middleware, allowing support forĀ Page Routing.

Implementing Kentico Xperience StatusCodePages Middleware

AsĀ recommended by Microsoft, we’ll start implementing our Kentico Xperience StatusCodePages Middleware by defining an extension method that targetsĀ IApplicationBuilder, to expose the usage ofĀ UseStatusCodePages(IApplicationBuilder, Func<StatusCodeContext,Task>)Ā to configure the StatusCodePages middleware.

We’ll start our implementation with the WithRedirects extension:

				
					public static class XperienceStatusCodePagesExtensions
{

  private static async Task GetErrorNodePathAsync( HttpContext context )
  {
    var errorPageRetriever = context.RequestServices.GetRequiredService();
    var page = await errorPageRetriever.RetrieveAsync( context.Response.StatusCode, context.RequestAborted );
    if( page is null )
    {
      return string.Empty;
    }

    var pageUrlRetriever = context.RequestServices.GetRequiredService();
    return pageUrlRetriever.Retrieve( page )
      ?.RelativePath
      ?.TrimStart( '~' );
  }

  public static IApplicationBuilder UseXperienceStatusCodePageWithRedirects( this IApplicationBuilder app )
  {
    if( app is null ) throw new ArgumentNullException( nameof( app ) );

    return app.UseStatusCodePages(
      async context =>
      {
        var path = await GetErrorNodePathAsync( context.HttpContext );
        if( !string.IsNullOrEmpty( path ) )
        {
          context.HttpContext.Response.Redirect( context.HttpContext.Request.PathBase + path );
        }
      }
    );
  }

}
				
			

If the path of anĀ HttpErrorNodeĀ can be retrieved viaĀ GetErrorNodePathAsync, we instruct the response to redirect to the retrieved path. InĀ GetErrorNodePathAsync, we use theĀ IErrorPageRetrieverĀ fromĀ Part IĀ to retrieve anĀ HttpErrorNodeĀ for the current response, and Kentico’sĀ IPageUrlRetrieverĀ to retrieve the path for the respectiveĀ HttpErrorNode. By using Kentico’sĀ IPageUrlRetriever, our Kentico Xperience StatusCodePages Middleware will automatically support theĀ HttpErrorNode'sĀ configuredĀ Page RoutingĀ behavior.

To learn more about the details of Kentico’sĀ IPageUrlRetrieverĀ service, check out Sean G. Wright’s excellent piece “Bits of Xperience: The Hidden Cost of IPageUrlRetriever.Retrieve“.

That staticĀ GetErrorNodePathAsyncĀ method though,Ā kind of gross. Just as was done with theĀ ErrorControllerĀ example from Part I, a “Retriever” service can be defined to make the implementation more concise and extensible.

				
					public interface IErrorPageUrlRetriever
{

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

}
Refactoring the code from the static GetErrorNodePathAsync method, the ErrorPageUrlRetriever is as follows:
public class ErrorPageUrlRetriever : IErrorPageUrlRetriever
{
  #region Fields
  private readonly IErrorPageRetriever errorPageRetriever;
  private readonly IPageUrlRetriever pageUrlRetiever;
  #endregion

  public ErrorPageUrlRetriever(
      IErrorPageRetriever errorPageRetriever,
      IPageUrlRetriever pageUrlRetiever
  )
  {
    this.errorPageRetriever = errorPageRetriever;
    this.pageUrlRetriever = pageUrlRetriever;
  }

  public async Task RetrieveAsync( int code, CancellationToken cancellation = default )
  {
    var page = await errorPageRetriever.RetrieveAsync( code, cancellation );
    if( page is null )
    {
      return null;
    }

    return pageUrlRetriever.Retrieve( page );
  }

}
				
			

Add our service to the IServiceCollection in the Startup, and we’ve once again made our code a bit more concise.

				
					public class Startup
{

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

    services.AddTransient();
    services.AddTransient();

    // ...
  }

}
public static class XperienceStatusCodePagesExtensions
{

  private static async Task GetErrorNodePathAsync( HttpContext context )
  {
    var url = await context.RequestServices.GetRequiredService()
      .RetrieveAsync( context.Response.StatusCode, context.RequestAborted );

    return url?.RelativePath?.TrimStart( '~' );
  }

  // ...
}
				
			

The ASP.NET Core source code for theĀ UseStatusCodePagesWithReExecuteĀ extension method onĀ GitHubĀ shall serve as a solid reference for how to supportĀ ReExecuteĀ functionality. Reviewing the source code, there areĀ 4 linesĀ of particular interest:

				
					var newPath = new PathString(
  string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode));
var formatedQueryString = queryFormat == null
  ? null : string.Format(CultureInfo.InvariantCulture, queryFormat, context.HttpContext.Response.StatusCode);
var newQueryString = queryFormat == null ? QueryString.Empty : new QueryString(formatedQueryString);

// ...
				
			

These 4 lines format the string arguments passed toĀ UseStatusCodePagesWithReExecuteĀ that create the path toĀ ReExecuteĀ the request pipeline on. We can base our implementation off of the framework’s source for UseStatusCodePagesWithReExecute, replacing these four lines with a call to ourĀ GetErrorNodePathAsyncĀ method. This will ensure that our custom Kentico Xperience StatusCodePages Middleware’s behavior is compatible with the behavior of the StatusCodePages Middleware when usingĀ UseStatusCodePagesWithReExecuteĀ out-of-the-box, ensuring additional middleware in the request pipeline that may depend on theĀ IStatusCodeReExecuteFeatureĀ will continue to behave correctly, just as expected.

				
					public static class XperienceStatusCodePagesExtensions
{
  // ...

  public static IApplicationBuilder UseXperienceStatusCodePageWithReExecute( this IApplicationBuilder app )
  {
    if( app is null ) throw new ArgumentNullException( nameof( app ) );

    return app.UseStatusCodePages(
      async context =>
      {
        // Get the path to the HttpErrorNode
        var path = await GetErrorNodePathAsync( context.HttpContext );
        var newPath = new PathString( path );

        var originalPath = context.HttpContext.Request.Path;
        var originalQueryString = context.HttpContext.Request.QueryString;

        // Store the original paths so the app can check it.
        context.HttpContext.Features.Set(
          new StatusCodeReExecuteFeature
          {
            OriginalPathBase = context.HttpContext.Request.PathBase.Value,
            OriginalPath = originalPath.Value,
            OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
          }
        );

        // An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline, we need to reset the endpoint and route values to ensure things are re-calculated.
        context.HttpContext.SetEndpoint(endpoint: null);

        context.HttpContext.Features.Get()
          ?.RouteValues
          ?.Clear();

        context.HttpContext.Request.Path = newPath;
        context.HttpContext.Request.QueryString = QueryString.Empty;

        try
        {
          await context.Next( context.HttpContext );
        }
        finally
        {
          context.HttpContext.Request.Path = originalPath;
          context.HttpContext.Request.QueryString = originalQueryString;

          context.HttpContext.Features.Set(null);
        }
      }
    );
  }

}
				
			

The next step is to register theĀ HttpErrorNodeĀ for Kentico XperienceĀ Page Routing, refactoring theĀ ErrorControllerĀ to use Kentico’sĀ IPageDataContextRetriever.

				
					[assembly: RegisterPageRoute( HttpErrorNode.CLASS_NAME, typeof( ErrorController ) )]
public class ErrorController
{

  public IActionResult Index( [FromServices] IPageDataContextRetriever contextRetriever )
  {
    contextRetriever.TryRetrieve( out IPageDataContext data );
    var viewModel = new ErrorViewModel
    {
      Content = new HtmlContentBuilder()
        .SetHtmlContent(data?.Page?.Content ?? "There was an error processing the request."),
      Heading = data?.Page?.Heading ?? "Internal Server Error"
    }

    return View( viewModel );
  }

}
				
			
				
					@model ErrorViewModel 

<h1>@Model.Heading</h1>
<p>@Model.Content</p> 
				
			

Finally, update the Startup.cs to use our desired Kentico Xperience StatusCodePages middleware behavior:

				
					public class Startup
{
  // ...

  public void Configure( IApplicationBuilder app )
  {
    //app.UseXperienceStatusCodePagesWithRedirects();
    app.UseXperienceStatusCodePagesWithReExecute();

    // ...
  }
}
				
			

Requests to the MVCMvc site at paths that produce a 404: Not Found status code will now return the Content and Heading from an HttpErrorNode where HttpStatusCode = 404:

				
					GET https://localhost:5001/not-a-real-url


<!DOCTYPE html>
  <html>
    <head>
      <!-- ... -->
    </head> 
    <body>
      <h1>Page Not Found</h1>
      <p>...</p>
    </body> 
  </html> 
				
			

Learn More on GitHub

Learn more about ASP.NET Core StatusCodePages + Kentico Xperience Middleware on GitHub atĀ BizStream/xperience-status-code-pages. Stay up to date on implementation details, open a PR, or just ask a question!

In Part 3

InĀ Part III: Using the BizStream Kentico Xperience StatusCodePages Packages, we’ll dig into theĀ BizStream/xperience-status-code-pagesĀ repository, and how the NuGet packages published from it can be used to quickly get started using StatusCodePages powered by Kentico Xperience.

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

About the Author

Sam Steele

Subscribe to Our Blog

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