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.
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( '~' );
}
// ...
}
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
@Model.Heading
@Model.Content
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
Page Not Found
...
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 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.
We love to make cool things with cool people. Have a project you’d like to collaborate on? Let’s chat!
Stay up to date on what BizStream is doing and keep in the loop on the latest in marketing & technology.