Create ASP.NET Core custom rendering engine middleware to display a content-managed not found page for Experience Edge
It is well-known that the not found page is displayed whenever a user types a non-existent domain address in the browser url. Over the years, 404 pages have become creative as specified in the best 404 pages.
Sometimes the most simple things could get left-out in the process of covering some of the other complicated things. In this blog post, I cover one such scenario of displaying the content-managed not found page on the asp.net rendering host (Sitecore MVP site as example) but has to check and fetch (using GraphQL) the not found page only when a route/path doesn't exist. Thanks to Rob Earlam for explaining this issue in a concise manner and then reviewing the PR to suggest approach-2 that was finally implemented!
Initially when I ventured to build such a simple use-case, all I could find were suggestions to use Response. Redirect or to write a Sitecore Pipeline Processor. In short, I actually couldn't find any examples to display a not found page that is compatible with the new XM Cloud/Experience Edge-related decoupled architecture. Also, since this was an issue in the MVP site, I thought it would be good exercise to get my hands dirty. As ever, this blog post covers my approach for implementing a not found page in the decoupled architecture.
General rule of thumb for headless architecture:
First of all, here is the premise: I use the XM Cloud repo' MVP Site for this use-case since the existing MVP Site, displays a static not found page as of now.
#these params must be passed | |
query LayoutQuery($path: String!, $language: String!, $site: String!) { | |
layout(routePath: $path, language: $language, site: $site) { | |
item { | |
rendered | |
} | |
} | |
} |
using Microsoft.Extensions.Configuration; | |
using Mvp.Foundation.Configuration.Rendering.AppSettings; | |
using Mvp.Foundation.DataFetching.GraphQL; | |
using Sitecore.LayoutService.Client.Request; | |
using Sitecore.LayoutService.Client.RequestHandlers.GraphQL; | |
using System; | |
using System.Threading.Tasks; | |
namespace Mvp.Project.MvpSite.Middleware | |
{ | |
public class CustomGraphQlLayoutServiceHandler | |
{ | |
private readonly IGraphQLRequestBuilder _graphQLRequestBuilder; | |
private readonly IGraphQLClientFactory _graphQLClientFactory; | |
private readonly MvpSiteSettings _configuration; | |
public CustomGraphQlLayoutServiceHandler(IConfiguration configuration, IGraphQLRequestBuilder graphQLRequestBuilder, IGraphQLClientFactory graphQLClientFactory) | |
{ | |
_graphQLRequestBuilder = graphQLRequestBuilder; | |
_graphQLClientFactory = graphQLClientFactory; | |
_configuration = configuration.GetSection(MvpSiteSettings.Key).Get<MvpSiteSettings>(); | |
} | |
public async Task<bool> CheckLayoutExists( | |
SitecoreLayoutRequest layoutrequest) | |
{ | |
try | |
{ | |
var client = _graphQLClientFactory.CreateGraphQlClient(); | |
var query = "\r\n query LayoutQuery($path: String!, $language: String!, $site: String!) {\r\n layout(routePath: $path, language: $language, site: $site) {\r\n item {\r\n rendered\r\n }\r\n }\r\n }"; | |
var variables = (object)new | |
{ | |
path = layoutrequest.Path(), | |
language = layoutrequest.Language(), | |
site = _configuration.DefaultSiteName | |
}; | |
var request = _graphQLRequestBuilder.BuildRequest(query, variables); | |
var graphQlResponse = await client.SendQueryAsync<LayoutQueryResponse>(request); | |
string str = graphQlResponse?.Data?.Layout?.Item?.Rendered.ToString(); | |
if (str == null) return false; | |
} | |
catch(Exception ex) | |
{ | |
var x = ex.Message;//log this? | |
return false; | |
} | |
return true; | |
} | |
} | |
} | |
Approach-2:
using Microsoft.AspNetCore.Http; | |
using Sitecore.AspNet.RenderingEngine.Configuration; | |
using Sitecore.AspNet.RenderingEngine; | |
using Sitecore.LayoutService.Client; | |
using System.Threading.Tasks; | |
using Microsoft.Extensions.Options; | |
using Microsoft.AspNetCore.Mvc.Rendering; | |
using Microsoft.AspNetCore.Mvc; | |
using Sitecore.LayoutService.Client.Response; | |
using System; | |
using Sitecore.LayoutService.Client.Request; | |
using System.Collections.Generic; | |
using Sitecore.AspNet.RenderingEngine.Middleware; | |
using Mvp.Foundation.DataFetching.GraphQL; | |
using Microsoft.Extensions.Configuration; | |
using Mvp.Foundation.Configuration.Rendering.AppSettings; | |
using System.Net; | |
namespace Mvp.Project.MvpSite.Middleware | |
{ | |
public class CustomRenderingEngineMiddleware | |
{ | |
private readonly RequestDelegate _next; | |
private readonly ISitecoreLayoutRequestMapper _requestMapper; | |
private readonly ISitecoreLayoutClient _layoutService; | |
private readonly RenderingEngineOptions _options; | |
private readonly IGraphQLClientFactory _graphQLClientFactory; | |
private readonly IGraphQLRequestBuilder _graphQLRequestBuilder; | |
private readonly IConfiguration _configuration; | |
public CustomRenderingEngineMiddleware(RequestDelegate next, ISitecoreLayoutClient layoutService, ISitecoreLayoutRequestMapper requestMapper, IOptions<RenderingEngineOptions> options, IGraphQLClientFactory graphQLClientFactory, IGraphQLRequestBuilder graphQLRequestBuilder, IConfiguration configuration) | |
{ | |
_next = next; | |
_requestMapper = requestMapper; | |
_options = options.Value; | |
_graphQLClientFactory = graphQLClientFactory; | |
_graphQLRequestBuilder = graphQLRequestBuilder; | |
_layoutService = layoutService; | |
_configuration= configuration; | |
} | |
public async Task Invoke(HttpContext httpContext, IViewComponentHelper viewComponentHelper, IHtmlHelper htmlHelper) | |
{ | |
SitecoreLayoutRequest sitecoreLayoutRequest = _requestMapper.Map(httpContext.Request); | |
if (httpContext.GetSitecoreRenderingContext() == null) | |
{ | |
SitecoreLayoutResponse response = await GetSitecoreLayoutResponse(httpContext).ConfigureAwait(continueOnCapturedContext: false); | |
//Check not found page and set status code - start | |
if (response.Request.Path() == _configuration.GetSection(MvpSiteSettings.Key).Get<MvpSiteSettings>().NotFoundPage) { httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; } | |
//Check not found page and set status code - end | |
SitecoreRenderingContext renderingContext = new() | |
{ | |
Response = response, | |
RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper) | |
}; | |
httpContext.SetSitecoreRenderingContext(renderingContext); | |
} | |
else | |
{ | |
httpContext.GetSitecoreRenderingContext().RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper); | |
} | |
foreach (Action<HttpContext> postRenderingAction in (IEnumerable<Action<HttpContext>>)_options.PostRenderingActions) | |
postRenderingAction(httpContext); | |
httpContext.Items.Add((object)nameof(RenderingEngineMiddleware), (object)null); | |
await _next(httpContext).ConfigureAwait(continueOnCapturedContext: false); | |
} | |
private async Task<SitecoreLayoutResponse> GetSitecoreLayoutResponse(HttpContext httpContext) | |
{ | |
//intercept for not found logic - start | |
CustomGraphQlLayoutServiceHandler customGraphQlLayoutServiceHandler = new(_configuration,_graphQLRequestBuilder, _graphQLClientFactory); | |
SitecoreLayoutRequest sitecoreLayoutRequest = _requestMapper.Map(httpContext.Request); | |
bool retVal = await customGraphQlLayoutServiceHandler.CheckLayoutExists(sitecoreLayoutRequest); | |
if (!retVal) httpContext.Request.Path = _configuration.GetSection(MvpSiteSettings.Key).Get<MvpSiteSettings>().NotFoundPage; | |
//intercept for not found logic - end | |
sitecoreLayoutRequest = _requestMapper.Map(httpContext.Request); | |
return await _layoutService.Request(sitecoreLayoutRequest).ConfigureAwait(continueOnCapturedContext: false); | |
} | |
} | |
} |
using Microsoft.AspNetCore.Http; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.AspNetCore.Mvc.Rendering; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.Options; | |
using Mvp.Foundation.Configuration.Rendering.AppSettings; | |
using Mvp.Foundation.DataFetching.GraphQL; | |
using Sitecore.AspNet.RenderingEngine; | |
using Sitecore.AspNet.RenderingEngine.Configuration; | |
using Sitecore.AspNet.RenderingEngine.Middleware; | |
using Sitecore.LayoutService.Client; | |
using Sitecore.LayoutService.Client.Exceptions; | |
using Sitecore.LayoutService.Client.Request; | |
using Sitecore.LayoutService.Client.Response; | |
using System; | |
using System.Collections.Generic; | |
using System.Net; | |
using System.Threading.Tasks; | |
namespace Mvp.Project.MvpSite.Middleware | |
{ | |
public class CustomRenderingEngineMiddleware | |
{ | |
private readonly RequestDelegate _next; | |
private readonly ISitecoreLayoutRequestMapper _requestMapper; | |
private readonly ISitecoreLayoutClient _layoutService; | |
private readonly RenderingEngineOptions _options; | |
private readonly IGraphQLClientFactory _graphQLClientFactory; | |
private readonly IGraphQLRequestBuilder _graphQLRequestBuilder; | |
private readonly IConfiguration _configuration; | |
public CustomRenderingEngineMiddleware(RequestDelegate next, ISitecoreLayoutClient layoutService, ISitecoreLayoutRequestMapper requestMapper, IOptions<RenderingEngineOptions> options, IGraphQLClientFactory graphQLClientFactory, IGraphQLRequestBuilder graphQLRequestBuilder, IConfiguration configuration) | |
{ | |
_next = next; | |
_requestMapper = requestMapper; | |
_options = options.Value; | |
_graphQLClientFactory = graphQLClientFactory; | |
_graphQLRequestBuilder = graphQLRequestBuilder; | |
_layoutService = layoutService; | |
_configuration= configuration; | |
} | |
public async Task Invoke(HttpContext httpContext, IViewComponentHelper viewComponentHelper, IHtmlHelper htmlHelper) | |
{ | |
SitecoreLayoutRequest sitecoreLayoutRequest = _requestMapper.Map(httpContext.Request); | |
if (httpContext.GetSitecoreRenderingContext() == null) | |
{ | |
SitecoreLayoutResponse response = await GetSitecoreLayoutResponse(httpContext).ConfigureAwait(continueOnCapturedContext: false); | |
//Check not found page and set status code - start | |
if (response.HasErrors) | |
{ | |
foreach (SitecoreLayoutServiceClientException error in response.Errors) | |
{ | |
switch (error) | |
{ | |
case ItemNotFoundSitecoreLayoutServiceClientException: | |
httpContext.Request.Path = _configuration.GetSection(MvpSiteSettings.Key).Get<MvpSiteSettings>().NotFoundPage; | |
sitecoreLayoutRequest = _requestMapper.Map(httpContext.Request); | |
response = await GetSitecoreLayoutResponse(httpContext).ConfigureAwait(continueOnCapturedContext: false); | |
if (response.Request.Path() == _configuration.GetSection(MvpSiteSettings.Key).Get<MvpSiteSettings>().NotFoundPage) { httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; } | |
break; | |
default: | |
throw error; | |
} | |
} | |
} | |
//Check not found page and set status code - end | |
SitecoreRenderingContext renderingContext = new() | |
{ | |
Response = response, | |
RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper) | |
}; | |
httpContext.SetSitecoreRenderingContext(renderingContext); | |
} | |
else | |
{ | |
httpContext.GetSitecoreRenderingContext().RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper); | |
} | |
foreach (Action<HttpContext> postRenderingAction in (IEnumerable<Action<HttpContext>>)_options.PostRenderingActions) | |
postRenderingAction(httpContext); | |
httpContext.Items.Add(nameof(RenderingEngineMiddleware), null); | |
await _next(httpContext).ConfigureAwait(continueOnCapturedContext: false); | |
} | |
private async Task<SitecoreLayoutResponse> GetSitecoreLayoutResponse(HttpContext httpContext) | |
{ | |
SitecoreLayoutRequest sitecoreLayoutRequest = _requestMapper.Map(httpContext.Request); | |
return await _layoutService.Request(sitecoreLayoutRequest).ConfigureAwait(continueOnCapturedContext: false); | |
} | |
} | |
} |
Understandably, the custom pipeline calls the "Use" method that in turn invokes the middleware:
using Microsoft.AspNetCore.Builder; | |
using Mvp.Project.MvpSite.Extensions; | |
using Sitecore.AspNet.RenderingEngine.Middleware; | |
namespace Mvp.Project.MvpSite.Middleware | |
{ | |
public class CustomRenderingEnginePipeline:RenderingEnginePipeline | |
{ | |
// | |
// Summary: | |
// Adds the Sitecore Rendering Engine features to the given Microsoft.AspNetCore.Builder.IApplicationBuilder. | |
// | |
// | |
// Parameters: | |
// app: | |
// The Microsoft.AspNetCore.Builder.IApplicationBuilder to add features to. | |
public override void Configure(IApplicationBuilder app) | |
{ | |
app.UseCustomSitecoreRenderingEngine(); | |
} | |
} | |
} |
public static IApplicationBuilder UseCustomSitecoreRenderingEngine(this IApplicationBuilder app) | |
{ | |
app.UseMiddleware<CustomRenderingEngineMiddleware>(); | |
return app; | |
} |
Comments
Post a Comment