Loading...

Server side rendering with angular and .NET

The problem with Single Page Applications

There was a lot of criticism regarding AngularJS and the first generation of SPA frameworks that starting to get popular on 2009.
One of the main issues developers had with those frameworks are the slow initial load and the poor Search Engine Optimization or SEO.
Let's examine how the new generation frameworks: react and Angular managed to solve those problems

Slow initial load & SEO problem

So the first generation spa frameworks when you typed the url of the app, the server would return an html similar to this

        
            <html>
                <body>
                    <script src="the-spa.js"></script>
                </body>
            </html>
        
    

so the html we got was pretty much empty, and the dude responsible to show us the screen was the js file.
So we had to download the html, then download the js (which was usually large sized). then run the js, then the screen will show.
This process was horribly slow on mobile phones especially with the old frameworks which had poor performance. Also it's considered poor seo to let the google bot render the js and crawl our site from the rendered js. The bot rendering can be slow and not accurate and it will damage our search rating.

What is Server Side Rendering

The new generation frameworks are universal meaning the server side can also run the JS code of the frameworks .
This means that when we create our angular component, the server can understand the code and create html of our component.
This means that the initial load of our app can now give us full HTML of our screen. So we will load the full HTML and see our screen, at the bottom of the body tag there will be a call to download our script, we will run our script and switch the current screen with the screen from our rendered js.
So even mobile phones with slow connection will be able to get a fast HTML and will see the screen fast.

Server side rendering with .NET

So let's try and create a new @angular/cli application, and create server side rendering for our cli project using .NET and Visual Studio as IDE.

Starting a new project

We are going to create a new ASP .NET MVC project.
inside the project we are going to start a new project using @angular/cli: ng new ClientApp
@angular/cli can also generate a bootstrap files for the server side. You can create the universal project by typing in the terminal: ng g universal ssr
You will also need to install needed packages for server side rendering, the cli command already modified the package.json so just type: npm install In our visual studio we are going to add the files we created to our solution. We are also going to remove the node_modules folder from the solution otherwise visual studio will try to compile the cs files in there.

@angular/cli generate universal

So what did @angular/cli did when we ran the command: ng g universal <project-name>.
It created the following files

  • app.server.module.ts - Created this new file, this will be the root module that the server will run. It will wrap our previous root module and include ServerModule which will be needed by the server to render the HTML
  • main.server.ts - this will be the entry point file for the server to run it doesn't do to much just mainly runs our server module
  • tsconfig.server.json - the server project will be built with a slightly different typescript configuration, the main changes is the entryModule form angular compiler that is set to be the server module we created, and also that the module resolution will be with commonjs

@angular/cli also updated the following files:

  • package.json - in the package json we have a few new packages added where the main one is @angular/platform-server containing the ServerModule needed to run our angular code on the server.
  • .angular-cli.json - in the apps array we added a new app with the name we given our universal app, this app is set to use the tsconfig of the server and the entry point for the server main.server.ts
  • main.ts - the entry point for the browser is also updated, it is now set to launch the app module after the dom content loaded event happened.
  • app.module-ts - we modified the BrowserModule to run with the method withServerTransition so our browser module will create the switch between the dom created by the server and the one the browser created
main.server.ts

This file is the entry point file of our server.
We will need to set this file to work with our .NET server.
To do this we will first have to install a package called @nguniversal/aspnetcore-engine
The job of this package is to take our server module, the request and the root component selector and create an HTML from that, and do it in a way that .NET apps can run that js code.
Install the package with npm: npm install @nguniversal/aspnetcore-engine --save
Another package you will need to install is: npm install aspnet-prerendering --save

Run JS code with .NET

So our job is to take the main.server.ts and fill in the blanks which is to create a function, passing the function arguments that describe the request, this function needs to be run by our asp.net code.
For this to work we have to install a nugget called: Microsoft.AspNetCore.SpaServices this package contains helpers for building single page applications on ASP.NET MVC Core.
It gave me an error when first tried to install the SpaServices package, so I had to change the target framework to .NET Framework 4.7.
And of course you have to make sure that you have node installed on your system that is greater then version 6.
Another nuget package you will need will be: Microsoft.AspNetCore.SpaServices.Extensions

Let's start with running a simple node code and display it in our homescreen.
We are going to run the js code from inside the HomeController.cs
Inside the folder where the HomeController.cs is located, create a new JS file called hello.js with the following code:

        
            var prerendering = require('aspnet-prerendering');

            module.exports = prerendering.createServerRenderer(function(params) {
                return new Promise(function(resolve, reject) {
                    resolve({
                        html: "<h1>This is from node code</h1>"
                    })
                })    
            });
        
    

you will need to run: npm init --yes && npm install aspnet-prerendering --save in the folder where your HomeController.cs is located.
Code that we want to run with our .NET requires us to wrap that code with createServerRenderer located in the aspnet-prerendering package.
The createServerRenderer should return the result using a Promise, and calling the Promise resolve method.
Our C# code will call the function in the createServerRenderer and we can use the params argument to pass data from the C# server side to our JS code that runs on the server.
The params we can pass have the following structure:

        
            export interface BootFuncParams {
                location: any;
                origin: string;
                url: string;
                baseUrl: string;
                absoluteUrl: string;
                domainTasks: Promise<any>;
                data: any;
            }
        
    

We can also send data from the JS code back to the C# code by calling the resolve of the Promise.
We can pass the following data structure back to our C# code:

        
            export interface RenderToStringResult {
                html: string;
                statusCode?: number;
                globals?: {
                    [key: string]: any;
                };
            }
            export interface RedirectResult {
                redirectUrl: string;
            }
        
    

So when we want to create a JS code that will be run by our C# we need to return from the JS code an export default of createServerRenderer passing the function that will be called from C# and passing params to it, our code will run and we will resolve the response back.
In the file that we created we return an H1 tag with a message we would like to display in the rendered page from the server.

We can register the NodeServices and the SPA services in the Startup.cs file which will look like this:

        
            using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;

    namespace ssr2
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }

            public IConfiguration Configuration { get; }

            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddMvc();
                services.AddNodeServices();
                services.AddSpaPrerenderer();
            }

            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                }

                app.UseStaticFiles();

                app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");
                });
            }
        }
    }
        
    

notice that we added the node services and spa prerender services so we can later inject them in our controller.

Now in our HomeController.cs modify the Index to run our JS code.

        
            using System.Diagnostics;
            using System.Threading.Tasks;
            using Microsoft.AspNetCore.Mvc;
            using Microsoft.AspNetCore.SpaServices.Prerendering;
            using ssr2.Models;

            namespace ssr2.Controllers
            {
                public class HomeController : Controller
                {

                    public async Task<IActionResult> Index([FromServices] ISpaPrerenderer prerenderer)
                    {
                        var result = await prerenderer.RenderToString("./Controllers/home.js");
                        ViewData["PrerenderedHtml"] = result.Html;
                        return View();
                    }

                    public IActionResult About()
                    {
                        ViewData["Message"] = "Your application description page.";

                        return View();
                    }

                    public IActionResult Contact()
                    {
                        ViewData["Message"] = "Your contact page.";

                        return View();
                    }

                    public IActionResult Error()
                    {
                        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
                    }
                }
            }
        
    

We just change the Index method to be async, injected the ISpaPrerenderer service, and used it to run our JS code.

Running angular

OK so we managed to run JS code with our ASP.NET and pass HTML from the js code back to our server to be displayed.
We already created our angular application using the cli, and we also used the cli to generate the universal app structure.
What we need to to modify the main.server.ts so the code will be runnable on our C# side.
We will use ngAspnetCoreEngine to render our ServerModule this will return a promise with the response, and we can pass the html back to the server.
so a simplified main.server.ts will now look like this.

        
            import 'zone.js/dist/zone-node';
            import { enableProdMode } from '@angular/core';
            import { environment } from './environments/environment';
            import { AppServerModule } from './app/app.server.module';
            import { createServerRenderer } from 'aspnet-prerendering';
            import { ngAspnetCoreEngine, IEngineOptions } from '@nguniversal/aspnetcore-engine';
            
            if (environment.production) {
                enableProdMode();
            }
            
            export default createServerRenderer((params) => {
                console.log('!!!!!!');
                console.log(JSON.stringify(params));
            
                const setupOptions: IEngineOptions = {
                    appSelector: '<app-root></app-root>',
                    ngModule: AppServerModule,
                    request: params
                };
            
            
                return ngAspnetCoreEngine(setupOptions).then((response) => {
                    return {
                        html: response.html
                    }
                })
            });
        
    

The options we can transfer the ngAspnetCoreEngine are:

        
            export interface IEngineOptions {
                appSelector: string;
                request: IRequestParams;
                url?: string;
                document?: string;
                ngModule: Type<{}> | NgModuleFactory<{}>;
                providers?: StaticProvider[];
            }
        
    

Notice also that we need to pass a request to the IEngineOptions and we need to pass it from the C# code as our params of the function inside the createServerRenderer.
This data should be from the C# code, and the requst looks like this IRequestParams:

        
            export interface IRequestParams {
                location: any;
                origin: string;
                url: string;
                baseUrl: string;
                absoluteUrl: string;
                domainTasks: Promise<any>;
                data: any;
            }
        
    

We can't run main.server.ts code directly in our C# code, we have to first transform the typescript code to JS and run the compiled JS.
In the terminal type:

        
            ng build --app ssr
        
    

the compiled js filename by default is: main.bundle.js so let's try and run this file with our C# code.
Modify the HomeController.cs like so:

        
            using System.Diagnostics;
            using System.Threading.Tasks;
            using Microsoft.AspNetCore.Mvc;
            using Microsoft.AspNetCore.SpaServices.Prerendering;
            using ssr2.Models;
            
            namespace ssr2.Controllers
            {
                public class HomeController : Controller
                {
            
                    public async Task<IActionResult> Index([FromServices] ISpaPrerenderer prerenderer)
                    {
                        IRequest request = new IRequest();
                        request.url = this.Request.Path;
                        var result = await prerenderer.RenderToString("./ClientApp/dist-server/main.bundle.js", null, request);
                        ViewData["PrerenderedHtml"] = result.Html;
                        return View();
                    }
            
                    public IActionResult About()
                    {
                        ViewData["Message"] = "Your application description page.";
            
                        return View();
                    }
            
                    public IActionResult Contact()
                    {
                        ViewData["Message"] = "Your contact page.";
            
                        return View();
                    }
            
                    public IActionResult Error()
                    {
                        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
                    }
                }
            }
        
    

IRequest is a simple model where i can put extra data to be passed in the params

        
            using System;
            namespace ssr2.Models
            {
                public class IRequest
                {
                    public String url { get; set; }
            
                }
            }
        
    

Try and launch your app and you should see the cli starter screen displayed as a full rendered HTML page

Installing module just for browser

What happens if you install a module that can only run on the browser side.
Let's try and install ng-lottie.
Install the package using npm: npm install --save ng-lottie
Modify the app.module.ts to contain the new module in the imports array

        
            import { BrowserModule } from '@angular/platform-browser';
            import { NgModule } from '@angular/core';
            import { AppComponent } from './app.component';
            import {LottieAnimationViewModule} from 'ng-lottie'
            
            
            @NgModule({
                declarations: [
                AppComponent
                ],
                imports: [
                BrowserModule.withServerTransition({ appId: 'serverApp' }),
                LottieAnimationViewModule.forRoot()
                ],
                providers: [],
                bootstrap: [AppComponent]
            })
            export class AppModule { }
        
    

build your ssr project again with: ng build --app ssr and try to run the app again on the server you should see an exception with the server side rendering

BrowserModule

We need to seperate between modules that can only run on the browser like Lottie. For this case we need to seperate between the module that runs on the browser and the module that run on the server.
Create a new file called app.browser.module.ts with the following code:

        
            import { NgModule } from '@angular/core';
            import { AppModule } from './app.module';
            import { AppComponent } from './app.component';
            import { PrebootModule } from 'preboot';
            import { LottieAnimationViewModule } from 'ng-lottie';

            @NgModule({
                bootstrap: [AppComponent],
                imports: [
                    PrebootModule.withConfig({ appRoot: 'app-root' }),
                    AppModule,
                    LottieAnimationViewModule.forRoot()
                ],
                providers: [
                
                ]
            })
            export class AppBrowserModule { }
        
    

Also we will need to change the main.ts which is the entry point for the browser to launch the browser module and the the app module.
The app module contains shared code and everything inside the app module should be runnable by server and browser. So our main.ts should look like so:

        
            import { enableProdMode } from '@angular/core';
            import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
            
            import { AppBrowserModule } from './app/app.browser.module';
            import { environment } from './environments/environment';
            
            if (environment.production) {
                enableProdMode();
            }
            
            document.addEventListener('DOMContentLoaded', () => {
                platformBrowserDynamic().bootstrapModule(AppBrowserModule)
                .catch(err => console.log(err));
            });
        
    

also make sure to remove the lottie module from the app module, we transfered that module to the browser module and it should not run with the server.

The app will now work but we have another problem, what if we want to use a component that is declared in the Lottie module.
obviously we won't be able to use components from the root module, so for this to work we will have to create that component dynamically.
First since the component we want to use is not exposed we will need to create a wrapper arround that component.
create a file called lottie-wrapper.component.ts with the following code:

        
                import { Component, Input, Output, EventEmitter } from '@angular/core';

                @Component({
                    selector: 'nz-lottie-view',
                    template: `
                    <lottie-animation-view
                        [options]="options"
                        [width]="width"
                        [height]="height"
                        (animCreated)="handleAnimation($event)">
                    </lottie-animation-view>
                    `
                })
                export class LottieWrapperComponent {
                    @Input() options: any;
                    @Input() width: any;
                    @Input() height: any;
                    @Output() animCreated: EventEmitter<any> = new EventEmitter();
                
                    handleAnimation = (event) => {
                        this.animCreated.emit(event);
                    }
                }
        
    

now add this component to the browser module and we will use this component instead of the lottie component.
We will also need to add this component to the entry components in that module since we will dynamically create this component in other modules.
This will allow you to use a component defined in a parent module in child modules.
the app.browser.module.ts now looks like so

        
                import { NgModule } from '@angular/core';
                import { AppModule } from './app.module';
                import { AppComponent } from './app.component';
                import { PrebootModule } from 'preboot';
                import { LottieAnimationViewModule } from 'ng-lottie';
                import { LottieWrapperComponent } from './lottie-wrapper.component';
                
                @NgModule({
                    declarations: [LottieWrapperComponent],
                    bootstrap: [AppComponent],
                    imports: [
                        PrebootModule.withConfig({ appRoot: 'app-root' }),
                        AppModule,
                        LottieAnimationViewModule.forRoot()
                    ],
                    providers: [
                    
                    ],
                    entryComponents: [LottieWrapperComponent]
                })
                export class AppBrowserModule { }
        
    

Now we will create another component in our app module which creates animation based on the demo in the lottie github
so create the file demo.component.ts with the following code

        
                import {Component, ComponentFactoryResolver, OnInit, ViewChild, ViewContainerRef, Optional, Inject, PLATFORM_ID, Injector} from '@angular/core';
                import { LottieWrapperComponent } from './lottie-wrapper.component';
                import { isPlatformBrowser } from '@angular/common';
                
                @Component({
                    selector: 'lottie-animation-view-demo-app',
                    template: ` 
                                <ng-template #placeholder></ng-template>
                            <div id="player">
                                <p>Speed: x{{animationSpeed}}</p>
                                <div class="range-container">
                                <input #range type="range" value="1" min="0" max="3" step="0.5"
                                    (change)="setSpeed(range.value)">
                                </div>
                                <button (click)="stop()">stop</button>
                                <button (click)="pause()">pause</button>
                                <button (click)="play()">play</button>
                            </div>`
                })
                
                export class DemoComponent implements OnInit {
                    @ViewChild('placeholder', {read: ViewContainerRef}) dynamicComponentsPlaceholder: ViewContainerRef;
                
                    public lottieConfig: Object;
                    private anim: any;
                    public animationSpeed: number = 1;
                
                    constructor(@Inject(PLATFORM_ID) private _platformId: Object, private _di: Injector) {
                        this.lottieConfig = {
                            path: '../assets/pinjump.json',
                            autoplay: true,
                            loop: true
                        };
                    }
                
                    public ngOnInit() {
                        if (isPlatformBrowser(this._platformId)) {
                            const componentFactory = this._di.get(ComponentFactoryResolver);
                            const componentFactory1 = componentFactory.resolveComponentFactory(LottieWrapperComponent);
                            let componentRef = this.dynamicComponentsPlaceholder.createComponent<LottieWrapperComponent>(componentFactory1);
                            componentRef.instance.animCreated.subscribe((event) => {
                                this.handleAnimation(event);
                            })
                            componentRef.instance.options = this.lottieConfig;
                            componentRef.instance.height = 300;
                            componentRef.instance.width = 600;
                        }
                    }
                
                    handleAnimation(anim: any) {
                        this.anim = anim;
                    }
                
                    stop() {
                        this.anim.stop();
                    }
                
                    play() {
                        this.anim.play();
                    }
                
                    pause() {
                        this.anim.pause();
                    }
                
                    setSpeed(speed: number) {
                        this.animationSpeed = speed;
                        this.anim.setSpeed(speed);
                    }
                
                }
        
    

So we are dynamically creating the component that can only be run in the browser and therfor is located in the browser module, don't forget to register the demo component in the app module (not in the browser module) and also place the selector of this component in the component template of the app component.

Summary

To summerize this extremly complex lesson...
Server side rendering is necessary for fast initial load and also SEO.
We can run JS code using our .NET project.
We can use angular cli to create universal application and run it with our .NET code.
We will create seperation of modules and create a browser module which will be bootstrapped by our browser as well as server module which will be bootstrapped by our server side.