Server-side rendering (SSR) under the hood — ASP.NET Core
Microsoft announced since the release of .NET 5 that it would discontinue the support of its Microsoft.AspNetCore.SpaServices and Microsoft.AspNetCore.NodeServices libraries. This means that server-side rendering, which requires the NodeServices, won’t be supported anymore out of the box.
I’ve migrated the code required for server-side rendering to a new git repository targetting .NET 6, which is hosted here. In this article I’ll try to explain how this code works.
The middleware
After setting up server-side rendering in your ASP.NET Core application, you’ll end up with following middleware pipeline:
This UseSpaPrerendering
middleware essentially maps another middleware onto the pipeline, which essentially looks like this:
At line 22, a new MemoryStream is initialized to be the new, temporary OutputBuffer. In case there is a next
middleware (which usually there isn’t), this middleware will be manipulating the wrong outputbuffer, leaving the original outputbuffer intact. This original outputbuffer contains the html from the angularindex.html
.
Then at line 39, a dictionary is composed which will contain the data that is needed to render the specific page in the angular app. At line 46 we check if there is a ISpaPrerenderingService
registered by the application and trigger the OnSupplyData
method. This method should populate the customData
dictionary.
Then at line 51, with the original HTML from the angular index.html
back in the outputbuffer, we trigger the Prerenderer.RenderToString()
method, passing the customData
along. Here’s where the magic starts.
Obtaining the NodeInstance
In the middleware, the nodeServiceFactory is called, which creates an object holding all information needed to run javascript through c#.
First, the constructor of the NodeServicesOptions
is called, which sets its NodeInstanceFactory
to create an HttpNodeInstance
After this, CreateNodeServices
is called, which passes the NodeInstanceFactory
to the constructor of NodeServicesImpl
. The NodeServicesImpl simply creates a NodeInstance, and invokes a specific (or the default) export of a certain CommonJS bundle (which will be the compiled version of your main.server.ts file).
The inner workings of HttpNodeInstance
The HttpNodeInstance starts an express server and allows you to invoke a specific export of a specific CommonJS module in the NodeJS environment.
Starting an express server
The base constructor is being called with the contents of the entrypoint-http.js file. This starts an express server and prints the following string on the console:
Now when .NET receives data from NodeJS through a console.log
, the data is tested against this regex to detect when the connection information has been received:
Now we can send HTTP requests from the ASP.NET Core backend to this endpoint, and NodeJS will process them.
Invoking the CommonJS export
The entrypoint-http.js essentially looks like this:
Here you can see that the moduleName
and exportedFunctionName
which we will send from .NET are being invoked at line 10, passing in the additional parameters we will be receiving from .NET (which includes customData
).
Calling the express server from .NET
In Prerenderer.RenderToString, we’re invoking the renderToString
method from the /Content/Node/prerenderer.js
file.
This will be the default
export you specified in your very own main.server.ts
. Here this bootModule function is invoked with the custom data you passed through .NET.
The entire Visual Studio Solution can be found here.