Server-side rendering (SSR) under the hood — ASP.NET Core

Pieterjan De Clippel
3 min readFeb 18, 2022

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:

Your Startup.cs after enabling server-side rendering

This UseSpaPrerendering middleware essentially maps another middleware onto the pipeline, which essentially looks like this:

The middleware underneath UseSpaPrerendering()

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

A NodeInstanceFactory is initialized

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:

Connection information of the express server is being printed onto 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:

Detect when the express server connection information is logged

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:

Reading + processing requests from ASP.NET Core

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.

--

--