Angular & ASP.NET Core Engine
This is an ASP.NET Core Engine for running Angular Apps on the server for server side rendering.
Installation
npm i --S @nguniversal/aspnetcore-engine @nguniversal/common
Example Application utilizing this Engine
Usage
Things have changed since the previous ASP.NET Core & Angular Universal usage. We're no longer using TagHelpers, but now invoking the main.server file from the Home Controller itself, and passing all the data down to .NET.
Within our main.server file, things haven't changed much, you still have your createServerRenderer()
function that's being exported (this is what's called within the Node process) which is expecting a Promise
to be returned.
Within that promise we simply call the ngAspnetCoreEngine itself, passing in our providers Array (here we give it the current url
from the Server, and also our Root application, which in our case is just <app></app>
).
import 'es6-promise';
import 'es6-shim';
import 'reflect-metadata';
import 'zone.js';
import { enableProdMode } from '@angular/core';
import { INITIAL_CONFIG } from '@angular/platform-server';
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
import { AppServerModule } from './app/app.server.module';
import { ngAspnetCoreEngine } from '@nguniversal/aspnetcore-engine';
enableProdMode();
export default createServerRenderer(params => {
const setupOptions: IEngineOptions = {
appSelector: '<app></app>',
ngModule: AppServerModule,
request: params,
providers: [
]
};
return ngAspnetCoreEngine(setupOptions).then(response => {
response.globals.transferData = createTransferScript({
someData: 'Transfer this to the client on the window.TRANSFER_CACHE {} object',
fromDotnet: params.data.thisCameFromDotNET
});
return ({
html: response.html,
globals: response.globals
});
});
});
Configuring the URL and Document
It is possible to override the default URL and document fetched when the rendering engine
is called. To do so, simply pass in a url
and/or document
string to the renderer as follows:
let url = 'http://someurl.com';
let doc = '<html><head><title>New doc</title></head></html>';
const setupOptions: IEngineOptions = {
appSelector: '<app></app>',
ngModule: ServerAppModule,
request: params,
providers: [
],
url,
document: doc
};
What about on the .NET side?
Previously, this was all done with TagHelpers and you passed in your main.server file to it: <app asp-prerender-module="dist/main.server.js"></app>
, but this hindered us from getting the SEO benefits of prerendering.
Because .NET has control over the Html, using the ngAspnetCoreEngine, we're able to pull out the important pieces, and give them back to .NET to place them through out the View.
Below is how you can invoke the main.server file which gets everything started:
Hopefully in the future this will be cleaned up and less code as well.
HomeController.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SpaServices.Prerendering;
using Microsoft.AspNetCore.NodeServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
namespace WebApplicationBasic.Controllers
{
public class HomeController : Controller
{
public async Task<IActionResult> Index()
{
var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>();
var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>();
var applicationBasePath = hostEnv.ContentRootPath;
var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>();
var unencodedPathAndQuery = requestFeature.RawTarget;
var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}";
TransferData transferData = new TransferData();
transferData.request = AbstractHttpContextRequestInfo(Request);
transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)";
var prerenderResult = await Prerenderer.RenderToString(
"/",
nodeServices,
new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"),
unencodedAbsoluteUrl,
unencodedPathAndQuery,
transferData,
30000,
Request.PathBase.ToString()
);
ViewData["SpaHtml"] = prerenderResult.Html;
ViewData["Title"] = prerenderResult.Globals["title"];
ViewData["Styles"] = prerenderResult.Globals["styles"];
ViewData["Meta"] = prerenderResult.Globals["meta"];
ViewData["Links"] = prerenderResult.Globals["links"];
ViewData["TransferData"] = prerenderResult.Globals["transferData"];
return View();
}
private IRequest AbstractHttpContextRequestInfo(HttpRequest request)
{
IRequest requestSimplified = new IRequest();
requestSimplified.cookies = request.Cookies;
requestSimplified.headers = request.Headers;
requestSimplified.host = request.Host;
return requestSimplified;
}
}
public class IRequest
{
public object cookies { get; set; }
public object headers { get; set; }
public object host { get; set; }
}
public class TransferData
{
public dynamic request { get; set; }
public object thisCameFromDotNET { get; set; }
}
}
Startup.cs : Make sure you add NodeServices to ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddNodeServices();
}
What updates do our Views need now?
Now we have a whole assortment of SEO goodness we can spread around our .NET application. Not only do we have our serialized Application in a String...
We also have <title>
, <meta>
, <link>'s
, and our applications <styles>
In our _layout.cshtml, we're going to want to pass in our different ViewData
pieces and place these where they needed to be.
Notice ViewData[]
sprinkled through out. These came from our Angular application, but it returned an entire HTML document, we want to build up our document ourselves so .NET handles it!
<!DOCTYPE html>
<html>
<head>
<base href="/" />
<title>@ViewData["Title"]</title>
@Html.Raw(ViewData["Meta"])
@Html.Raw(ViewData["Links"])
@Html.Raw(ViewData["Styles"])
</head>
<body>
@RenderBody()
@Html.Raw(ViewData["TransferData"])
@RenderSection("scripts", required: false)
</body>
</html>
Your Home View - where the App gets displayed:
You may have seen or used a TagHelper here in the past (that's where it used to invoke the Node process and everything), but now since we're doing everything
in the Controller, we only need to grab our ViewData["SpaHtml"]
and inject it!
This SpaHtml
was set in our HomeController, and it's just a serialized string of your Angular application, but only the <app>/* inside is all serialized */</app>
part, not the entire Html, since we split that up, and let .NET build out our Document.
@Html.Raw(ViewData["SpaHtml"])
<script src="~/dist/vendor.js" asp-append-version="true"></script>
@section scripts {
<script src="~/dist/main-client.js" asp-append-version="true"></script>
}
What happens after the App gets server rendered?
Well now, your Client-side Angular will take over, and you'll have a fully functioning SPA. (With all these great SEO benefits of being server-rendered) !
:sparkles:
Bootstrap
The engine also calls the ngOnBootstrap lifecycle hook of the module being bootstrapped, this is how the TransferData gets taken.
Check https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/master/Client/modules to see how to setup your Transfer classes.
@NgModule({
bootstrap: [AppComponent]
})
export class ServerAppModule {
ngOnBootstrap = () => {
console.log('bootstrapped');
}
}
Tokens
Along with the engine doing serializing and separating out the chunks of your Application (so we can let .NET handle it), you may have noticed we passed in the HttpRequest object from .NET into it as well.
This was done so that we could take a few things from it, and using dependency injection, "provide" a few things to the Angular application.
ORIGIN_URL
REQUEST
import { ORIGIN_URL, REQUEST } from '@nguniversal/aspnetcore-engine';
Make sure in your BrowserModule you provide these tokens as well, if you're going to use them!
@NgModule({
...,
providers: [
{
provide: ORIGIN_URL,
useFactory: (getOriginUrl)
}, {
provide: REQUEST,
useFactory: (getRequest)
}
]
} export class BrowserAppModule() {}
Don't forget that the server needs Absolute URLs for paths when doing Http requests! So if your server api is at the same location as this Angular app, you can't just do http.get('/api/whatever')
so use the ORIGIN_URL Injection Token.
import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine';
constructor(@Inject(ORIGIN_URL) private originUrl: string, private http: Http) {
this.http.get(`${this.originUrl}/api/whatever`)
}
As for the REQUEST object, you'll find Cookies, Headers, and Host (from .NET that we passed down in our HomeController. They'll all be accessible from that Injection Token as well.
import { REQUEST } from '@nguniversal/aspnetcore-engine/tokens';
constructor(@Inject(REQUEST) private request) {
}