Sitecore Rendering Engine - From Request to Response
Continuing on my previous blog posts, in this post I want to continue down the request handling flow to see how the Layout Service request is triggered and how that results in a page being rendered.
Making the request
As mentioned in earlier posts, the [UseSitecoreRendering] attribute in the fallback controller (out-of-the-box this is set on the Index method from the DefaultController) starts another Middleware component, the RenderingEngineMiddleware. This middleware is triggered for every request which gets processed by this fallback controller.
This RenderingEngineMiddleware is important, because this is the part of the code which actually triggers the call to the Layout Service. In below code snippet you can see this Middleware component, once invoked, will use the ISitecoreLayoutClient service (which is registered in the Startup.cs).
public async Task Invoke(
HttpContext httpContext,
IViewComponentHelper viewComponentHelper,
IHtmlHelper htmlHelper)
{
Assert.ArgumentNotNull<HttpContext>(httpContext, nameof (httpContext));
if (httpContext.Items.ContainsKey((object) nameof (RenderingEngineMiddleware)))
throw new ApplicationException(Sitecore.AspNet.RenderingEngine.Resources.Exception_InvalidRenderingEngineConfiguration);
if (httpContext.GetSitecoreRenderingContext() == null)
{
SitecoreLayoutResponse sitecoreLayoutResponse = await this.GetSitecoreLayoutResponse(httpContext).ConfigureAwait(false);
httpContext.SetSitecoreRenderingContext((ISitecoreRenderingContext) new SitecoreRenderingContext()
{
Response = sitecoreLayoutResponse,
RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper)
});
}
else
httpContext.GetSitecoreRenderingContext().RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper);
foreach (Action<HttpContext> postRenderingAction in (IEnumerable<Action<HttpContext>>) this._options.PostRenderingActions)
postRenderingAction(httpContext);
httpContext.Items.Add((object) nameof (RenderingEngineMiddleware), (object) null);
await this._next(httpContext).ConfigureAwait(false);
}
private async Task<SitecoreLayoutResponse> GetSitecoreLayoutResponse(
HttpContext httpContext)
{
SitecoreLayoutRequest request = this._requestMapper.Map(httpContext.Request);
Assert.NotNull<SitecoreLayoutRequest>(request);
return await this._layoutService.Request(request).ConfigureAwait(false);
}
This ISitecoreLayoutService will in turn trigger a request to the Layout Service and return it’s response as a SitecoreLayoutResponse object. As you can see, this response object is then stored in the HttpContext. As the HttpContext is something that lives throughout the entire request lifetime, this means from this moment forth it will be possible to retrieve the raw Layout Service response from the HttpContext. And that is exactly the way Sitecore also uses this response, because as you can see this Middleware component doesn’t do anything other than just requesting and storing that Layout Service object.
Binding the models
The next step in order to render a page, is populate the Route object parameter from the Index method in the fallback controller. This is done by using Model Binding (my previous post describes what this is).
Sitecore has chosen to use model binding for pretty much all of the context related models. I am not going to describe the entire process of how these models are registered for usage in model binding, that would be too much information, but I want to highlight two steps/classes in this flow. If we take the Route object for example, then we can find a SitecoreLayoutRouteBindingSource class which is responsible for populating the Route object with details from the ISitecoreRenderingContext object.
public override object? GetModel(
IServiceProvider serviceProvider,
ModelBindingContext bindingContext,
ISitecoreRenderingContext context)
{
Assert.ArgumentNotNull<IServiceProvider>(serviceProvider, nameof (serviceProvider));
Assert.ArgumentNotNull<ModelBindingContext>(bindingContext, nameof (bindingContext));
Assert.ArgumentNotNull<ISitecoreRenderingContext>(context, nameof (context));
Type modelType = bindingContext.get_ModelMetadata().get_ModelType();
Route route = context.Response?.Content?.Sitecore?.Route;
return route != null && modelType == typeof (Route) ? (object) route : (object) null;
}
Where does this context object come from? This GetModel method is invoked by the SitecoreLayoutModelBinder
public Task BindModelAsync(ModelBindingContext bindingContext)
{
Assert.ArgumentNotNull<ModelBindingContext>(bindingContext, nameof (bindingContext));
T obj1 = bindingContext.get_BindingSource() as T;
if (BindingSource.op_Equality((BindingSource) (object) obj1, (BindingSource) null))
obj1 = Activator.CreateInstance<T>();
ISitecoreRenderingContext renderingContext = bindingContext.get_HttpContext().GetSitecoreRenderingContext();
using (IServiceScope scope = ServiceProviderServiceExtensions.CreateScope(this._serviceProvider))
{
object model = obj1.GetModel(scope.ServiceProvider, bindingContext, renderingContext);
if (model != null)
{
ValidationStateDictionary validationState = bindingContext.get_ValidationState();
object obj2 = model;
ValidationStateEntry validationStateEntry = new ValidationStateEntry();
validationStateEntry.set_SuppressValidation(true);
validationState.Add(obj2, validationStateEntry);
bindingContext.set_Result(ModelBindingResult.Success(model));
}
else
{
...
}
}
return Task.CompletedTask;
}
This approach is used for many such objects, like the Response, Component, Context and, as we have seen, the Route object.
Almost there
We now finally have a pretty complete picture of how a request to the ASP.NET Core application results in Middleware components being triggered, the Sitecore Layout Service being called and used for Model Binding to finally have a fairly straight forward MVC Controller triggered and it’s parameters populated.
From here, the path to generating the result page is more simple. The populated Route object contains all the details necessary to render a page, it includes the different placeholders on the page, components and their content. If you take a quick peek at the View for this Index method, you will see this will use tag helpers to render different Sitecore placeholder components which, based on the name property given, will render parts of the Layout Service response.
I hope these posts have helped you get a better understanding of how the ASP.NET Core Rendering Host works and figure out how you can fully customize it for your needs.