Content

What is the plan

We will start with setting up an Angular 4 project and running it under Electron. Then we will create two web API projects using .NET Core 2 and prepare Electron startup script which will be able to run these APIs locally. Next step will be to invoke them from Angular’s service and display the output on the screen. Finally we will prepare a deployment package for Windows and Linux!

What do we need

The easiest way to create .Net Core 2.0 applications is to use Visual Studio 2017 update 3 (version 15.3). You will probably also need to download the .Net Core SDK 2.0. It contains the runtime and the command line tools needed for development.

Visual Studio 15.3 comes with a nice angular solution templates but I still find the Angular CLI (Command Line Interface) as a better way of developing angular apps. It’s a dedicated tool and for sure it’s easier to keep it up-to-date. To use the CLI first we need to install Node.js with NPM. Again the reason I’m not using the built-in Visual Studio instance of Node.js is that it gets out-dated very quickly. Node.js installer can be downloaded from its home page

Once you installed it we can proceed with Electron and Angular CLI installation. We want them both to be installed globally which can be achieved by running the following commands in your command line tool:

npm install -g electron
npm install -g @angular/cli

Website project

As I mentioned previously new VS comes with the pretty good Angular template with the ASP.NET Core 2.0 MVC backend. But for me times when front-end and back-end were combined in a one project are gone. It is much better idea to keep them both separated. Things are much better organized and you never know when you will need to implement a new front-end layer of your application for mobile purpose for example. With your back-end tied to your web project this could become a bit tricky…

That’s why I prefer having a pure web angular project and move back-end stuff to separate microservices.

Let’s create a new blank solution in VS and then add a new logical folder to it and create a new project in it. From available project templates choose the Visual C# -> Web -> ASP.NET Web Application and in the wizard choose the "Empty" template as shown in figure 1. I’ll name my solution as "AngularCoreMicroservicesElectron", the folder like "Frontend" and the project will be "AngularWebsite". Your solution should look like in figure 2.

Visual Studio ASP.NET template selection window with Empty template selected
Figure 1. Select empty ASP.NET template
Solution structure. Frontend folder with AngularWebsite and packages.config and web.config inside.
Figure 2. Solution structure

Next thing which we want to do is to disable compilation of typescript files by Visual Studio. This will be handled by the CLI automatically. We will still have intellisense for TS in VS; we are just turning off the compilation functionality which would be unnecessarily doubled. It’s much easier to update TS version used by the CLI than the one used by VS. To disable Visual Studio TypeScript compilation right click on the website project, unload it and again right click it and choose edit csproj. To the first PropertyGroup node add two highlited lines:

<PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProductVersion>
    </ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{2B8D79CD-D50C-429C-8854-759F37C0F6B2}</ProjectGuid>
    <ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>AngularWebsite</RootNamespace>
    <AssemblyName>AngularWebsite</AssemblyName>
    <TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
    <UseIISExpress>true</UseIISExpress>
    <Use64BitIISExpress />
    <IISExpressSSLPort />
    <IISExpressAnonymousAuthentication />
    <IISExpressWindowsAuthentication />
    <IISExpressUseClassicPipelineMode />
    <UseGlobalApplicationHostFile />
    <NuGetPackageImportStamp>
    </NuGetPackageImportStamp>
    <TypeScriptToolsVersion>latest</TypeScriptToolsVersion>
    <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup>
Save the file and reload the project. It’s time to use the CLI to generate and add angular to the project. Open a command line in your solution’s folder and run the following command:
ng new YourWebsiteProjectName –-skip-git --routing
Be patient. It’s going to retrieve all the necessary npm packages now so this will take some time. Console output should look like this:
"ng new AngularWebsite --ski-git --routing" command run in powershell finished successfully
Figure 3. Generating a new project with the CLI
Now, in VS, include all generated files and folders into your project except node_modules (you don’t need bin and obj folders neither). Let’s get back to the command line tool and run:
ng serve
Your angular page should be, after short while, hosted by the development web server under http://localhost:4200:
"ng serve" command successfully run in powershell and angular app launched in chrome
Figure 4. Default Angular template served in NG Dev Web Server

This chapter steps can be found under these three commits:

For more information regarding Angular CLI you should visit its homepage

Run angular CLI-hosted website in electron

Now let’s put Electron layer on our Angular website. Quick start guide for Electron which we are going to use for now can be found right here. Following its guidance let’s go to package.json file which is located in the root folder of our website project and change the version number to 0.1.0. We also need to add "main" entry which tells electron where it can find the entry point to our application. Its value should be "main.js". Package.json now looks like this:

{
  "name": "angular-website",
  "version": "0.1.0",
  "main": "main.js",
  "license": "MIT",
  […]
}

Now create main.js file next to package.json. We will paste there content from mentioned quick-start page. I’ll let myself to remove comments from that file because they take more space than the code itself:

const { app, BrowserWindow } = require('electron');
const path = require('path');
const url = require('url');

let win;

function createWindow() {
    win = new BrowserWindow({ width: 800, height: 600 });
    
    win.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
    }));
    
    win.webContents.openDevTools();
    
    win.on('closed', () => {
        win = null;
    });
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin')
        app.quit();
})

app.on('activate', () => {
    if (win === null)
        createWindow();
});
The code is pretty simple. On ready and on activate events new window is created which loads index.html file and opens dev tools. On window-all-closed event, which is fired when all windows are closed, the application is shutdown.

In a standard case scenario, where website is loaded from index.html file, the above code would work. But as we use Angular CLI’s web server to host our page we need to tell Electron to load it from http://localhost:4200. To do that change:

win.loadURL(url.format({
	pathname: path.join(__dirname, 'index.html'),
	protocol: 'file:',
	slashes: true
}));
to:
win.loadURL('http://localhost:4200');

Save changes. Let’s check if that works. Make sure your page is still hosted by CLI (the console with "ng serve" command is still running). Open another instance of a command line tool in the website directory and write:

electron .
New window with our app hosted in electron should appear as shown in figure 5.
"Electron ." command run in powershell. Electron window displaying angular application as a result.
Figure 5. Angular application displayed in Electron

Cool, huh? :)

This chapter is covered by this commit

Add microservices

Step by step we are getting closer to our goal. Next thing we should do is to add two simple microservices which actually will be two .Net Core 2 MVC Web APIs. Add a new logical folder to the solution and name it Backend. Create there a new project. From templates choose Visual C# -> Web -> ASP.NET Core Web Application and name it ItemsApi. In the wizard choose Web API template and click OK (make sure ASP.NET Core 2.0 is selected in the dropdown at the top of the window). Let’s rename ValuesController to ItemsController and make its Get() method return "Desk" and "Pen" instead of "value1" and "value2". Same for Get(int id) – let it return "Desk". Your project and this file should now look like in figure 6.

Current state of ItemsControler with Get() method returning Desk and Pen and Get(int id) method returning Desk. Also solution explorer window showing ItemsApi project in Backend folder.
Figure 6. Current state of ItemsController and Solution Explorer

As we renamed our default controller we should also change launchUrl in Properties/launchSettings.json. I’m also going to remove IIS settings from here as I want the project to be self hosted:

{
  "profiles": {
    "ItemsApi": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/items",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:50842/"
    }
  }
}

Save all changes and set ItemsApi as the startup project in the solution explorer and hit F5. A browser window should launch with the response from ItemsController’s Get() method like shown in figure 7.

localhost:50842/api.items request shown in a browser. In respons browser shows array with two elements: Desk and Pen
Figure 7. ItemsApi response

Great. Let’s add another service called ShapesApi. Apply the same changes as for ItemsApi with the difference that the controller should be named ShapesController and it should return "Rectangle" and „Square" from Get() method and "Rectangle" from Get(int id).

All changes for this step can be found in this commit.

Preparations for hosting

So far so good. We have the working Angular website hosted in Electron and two ASP.NET Core 2.0 MVC WebAPI microservices. Now take a look how to host these services from Electron’s main.js file.

We are going to use a feature that .Net Core applications can host themselves in the Kestrel HTTP server (you can read more about it here on ASP.NET Core Fundamentals page). ASP.NET Core applications compile to .dll files. To run them you can use "dotnet" command. For example if you compile ShapesApi and go to bin\debug\netcoreapp2.0 you can run it by executing following command in console:

dotnet .\ShapesApi.dll
Problem is that the Kestrel by default tries to bind all applications to the same 5000 port. If you try to run both APIs at the same time you get:
Unable to start Kestrel.
System.IO.IOException: Failed to bind to address http://127.0.0.1:5000: address already in use.

To fix this issue we need to install a package from nuget to the both API projects. The package is called Microsoft.Extensions.Configuration.CommandLine. After it is installed modify BuildWebHost method which, in both API projects, is in Program.cs file. Make it look like this:

public static IWebHost BuildWebHost(string[] args)
{
	var config = new ConfigurationBuilder()
					.AddCommandLine(args)
					.Build();

	return WebHost.CreateDefaultBuilder(args)
		.UseConfiguration(config)
		.UseStartup()
		.Build();
}
Thanks to this modification we are now able to tell Kestrel, through the command line, which port it should use to host the app. You can try below command and, if 8080 port is free, you should see:
dotnet .\ShapesApi.dll --server.urls=http://localhost:8080
Now listening on: http://localhost:8080

Such prepared we can switch back to Electron. First we need to tell it which services to host and where they are located. This will be for sure dependent on an environment on which we run our app. So we need to add some configuration. There is a module called dotenv available on NPM which lets to store configuration in an .env file.

Let’s install it executing below command in angular project's root folder:

npm install dotenv --save
Add .env file in angular project’s root folder and copy-paste following variables to it:
PRODUCTION=false
APIS=[{"name":"itemsApi","filePath":"..\\ItemsApi\\bin\\Debug\\netcoreapp2.0\\ItemsApi.dll"},{"name":"shapesApi","filePath":"..\\ShapesApi\\bin\\Debug\\netcoreapp2.0\\ShapesApi.dll"}]
APPLICATION_URL=http://localhost:4200
APPLICATION_FILE_PATH=src\index.html
APPLICATION_PORT_ERROR_FILE_PATH=src\portError.html
APPLICATION_API_ERROR_FILE_PATH=src\apiError.html
WINDOW_DEFAULT_WIDTH=1024
WINDOW_DEFAULT_HEIGHT=768
The most important one is of course APIS. It is a JSON which contains names and paths of APIs. Except that we also have:
  • PRODUCTION - Indicating whether the APP is run in the PROD environment
  • APPLICATION_URL – Link to a page which should be run in the Dev environment
  • APPLICATION_FILE_PATH – Path to file which should be run in the PROD environment
  • APPLICATION_PORT_ERROR_FILE_PATH – Path to a file which should be run if something goes wrong when looking for a free port
  • APPLICATION_API_ERROR_FILE_PATH – Path to a file which should be run if something goes wrong when launching an API
  • WINDOW_DEFAULT_WIDTH – starting width of the application window
  • WINDOW_DEFAULT_HEIGHT – starting height of the application window
All these settings, after invoking require('dotenv').config() are available under process.env variable in main.js file.

We should add portError.html and apiError.html to the src folder now as we can forget about it later :) My look like:

  • portError.html
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8" />
      <title>Can't find free port</title>
    </head>
    <body>
      <h1>Application was unable to find a free port on your machine. Please try restarting your computer.</h1>
    </body>
    </html>
    
  • apiError.html
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8" />
      <title>Can't start WebAPI</title>
    </head>
    <body>
      <h1>Application was unable to start the WebAPI on your machine. Please try restarting your computer.</h1>
    </body>
    </html>

There is one more module which will be needed – portfinder. It searches for free ports which we will use when starting APIs. Add it to website project by running:

npm install portfinder --save

Commits for this chapter:

Host ASP.NET Core 2.0 MVC microservices from Electron

With all this we can modify main.js to serve our purpose. I prepared working code (whole file below) which you can use as a base for your own solution. I’ll try to explain the most important parts. As you could notice in the previous version of main.js we can control our application by subscribing to the electron events. Entry point to the program is the ready event where we will create application’s main window and host the APIs. Except that we are going to use:

  • activate - on which we recreate the window and the APIs if not exist
  • window-all-closed - on which we quit the application

Here is the most important part of the code (I'll discuss details further):

let win = null;
let apis = [];
let launchedApis = {};

let apiProcessLoadingError = false;

app.on('ready', startup);
app.on('activate', recreateWindowAndApiIfEmpty);
app.on('window-all-closed', quitApplication);

function recreateWindowAndApiIfEmpty() {
    if (win === null)
        startup();
}

function quitApplication() {
    if (process.platform !== 'darwin')
        app.quit();
}

function startup() {
    createWindow();

    win.on('closed', cleanup);

    if (!isProductionEnvironment())
        showDevTools();

    if (apis.length === 0)
        getApis();

    if (apis.length > 0) {
        if (isLaunchedApisEmpty())
            startApisAndLoadContent();
    }
    else
        loadContent();
}

In the startup() method first thing is window creation. At this moment it is empty. In the next line, as we have created the window, we can subscribe to its closed event to kill the API processes if they are launched:

function cleanup() {
    if (!isLaunchedApisEmpty())
        tryKillApiProcesses();

    launchedApis = null;
    win = null;
}

Next, in startup(), we check if it is a production environment and if not then we turn on the chromium’s developer tools. When this is done we get the APIs data from the .env file. It is parsed to an object in getApis() method and stored under apis variable. If there are any APIs defined and they have not been launched yet (remember that this piece of code is also executed on activate event) then we start them and load the content. If there aren’t any then just load the content:

function isProductionEnvironment() {
    return process.env.PRODUCTION === "true";
}

function showDevTools() {
    win.webContents.openDevTools();
}

function getApis() {
    apis = JSON.parse(process.env.APIS);
}

function isLaunchedApisEmpty() {
    return launchedApis === null || Object.keys(launchedApis).length === 0;
}

If it comes to starting the APIs then for each of them we use Portfinder which invokes a callback function with a free port number:

function startApisAndLoadContent() {
    apis.forEach(function (api) {
        let freePortSuccessCallback = createStartApiAndLoadContentCallback(api);
        findFreePort(freePortSuccessCallback, loadErrorFindingFreePortPage);
    });
}

function createStartApiAndLoadContentCallback(api) {
    return function (port) {
        startApiAndLoadContent(api, port);
    }
}

function findFreePort(successCallback, errorCallback) {
    portfinder.getPortPromise()
        .then(successCallback)
        .catch(errorCallback);
}

In the success callback function we start the API by executing dotnet command by nodejs’ execFile function. This function returns ChildProcess object which represents launched process. If all of the APIs are launched then we also load window content. Every launched API process returned by execFile is stored in launchedApis associative array so we could know which processes to shutdown when exiting the application and to be able to notify our webpage (discussed in next chapter) on which ports the APIs are listening:

function startApiAndLoadContent(api, port) {
    if (win === null)
        return;

    startApi(api, port);

    if (areAllApisStarted())
        loadContent();
}

function startApi(api, port) {
    let args = buildApiArguments(api.filePath, port);

    let apiProcess = execFile('dotnet', args, function (error, stdout, stderr) {
        if (error) {
            handleApiLoadingError(error);
            return;
        }
        console.log(`WebAPI stdout: ${stdout}`);
        console.log(`WebAPI stderr: ${stderr}`);
    });

    launchedApis[api.name] = { process: apiProcess, port: port };
}

All of this combined together gives us whole new main.js file. Just copy-paste the following code to your main.js, as it is exactly what we have discussed above, replacing the previous content:

const { app, BrowserWindow, ipcMain } = require('electron');
const { execFile } = require('child_process');
const path = require('path');
const url = require('url');
const portfinder = require('portfinder');

require('dotenv').config();

let win = null;
let apis = [];
let launchedApis = {};

let apiProcessLoadingError = false;

app.on('ready', startup);
app.on('activate', recreateWindowAndApiIfEmpty);
app.on('window-all-closed', quitApplication);

function recreateWindowAndApiIfEmpty() {
    if (win === null)
        startup();
}

function quitApplication() {
    if (process.platform !== 'darwin')
        app.quit();
}

function startup() {
    createWindow();

    win.on('closed', cleanup);

    if (!isProductionEnvironment())
        showDevTools();

    if (apis.length === 0)
        getApis();

    if (apis.length > 0) {
        if (isLaunchedApisEmpty())
            startApisAndLoadContent();
    }
    else
        loadContent();
}

function createWindow() {
    let defaultWidth = parseInt(process.env.WINDOW_DEFAULT_WIDTH);
    let defaultHeight = parseInt(process.env.WINDOW_DEFAULT_HEIGHT);

    win = new BrowserWindow({
        width: defaultWidth,
        height: defaultHeight
    });
}

function isProductionEnvironment() {
    return process.env.PRODUCTION === "true";
}

function showDevTools() {
    win.webContents.openDevTools();
}

function getApis() {
    apis = JSON.parse(process.env.APIS);
}

function isLaunchedApisEmpty() {
    return launchedApis === null || Object.keys(launchedApis).length === 0;
}

function startApisAndLoadContent() {
    apis.forEach(function (api) {
        let freePortSuccessCallback = createStartApiAndLoadContentCallback(api);
        findFreePort(freePortSuccessCallback, loadErrorFindingFreePortPage);
    });
}

function createStartApiAndLoadContentCallback(api) {
    return function (port) {
        startApiAndLoadContent(api, port);
    }
}

function findFreePort(successCallback, errorCallback) {
    portfinder.getPortPromise()
        .then(successCallback)
        .catch(errorCallback);
}

function startApiAndLoadContent(api, port) {
    if (win === null)
        return;

    startApi(api, port);

    if (areAllApisStarted())
        loadContent();
}

function startApi(api, port) {
    let args = buildApiArguments(api.filePath, port);

    let apiProcess = execFile('dotnet', args, function (error, stdout, stderr) {
        if (error) {
            handleApiLoadingError(error);
            return;
        }
        console.log(`WebAPI stdout: ${stdout}`);
        console.log(`WebAPI stderr: ${stderr}`);
    });

    launchedApis[api.name] = { process: apiProcess, port: port };
}

function buildApiArguments(apiFilePath, port) {
    return [
        apiFilePath,
        `--server.urls=http://localhost:${port}`
    ];
}

function areAllApisStarted() {
    return apis.length === Object.keys(launchedApis).length;
}

function loadErrorFindingFreePortPage(error) {
    console.error(`portfinder error: ${error}`);
    loadWindowContentFromFile(process.env.APPLICATION_PORT_ERROR_FILE_PATH);
}

function handleApiLoadingError(error) {
    console.error(`WebAPI error: ${error}`);
    apiProcessLoadingError = true;
    loadWindowContentFromFile(process.env.APPLICATION_API_ERROR_FILE_PATH);
}

function loadContent() {
    if (apiProcessLoadingError)
        return;

    if (isProductionEnvironment())
        loadWindowContentFromFile(process.env.APPLICATION_FILE_PATH);
    else
        loadWindowContentFromHttpAddress(process.env.APPLICATION_URL);
}

function loadWindowContentFromFile(filePath) {
    win.loadURL(url.format({
        pathname: path.join(__dirname, filePath),
        protocol: 'file:',
        slashes: true
    }));
}

function loadWindowContentFromHttpAddress(url) {
    win.loadURL(url);
}

function cleanup() {
    if (!isLaunchedApisEmpty())
        tryKillApiProcesses();

    launchedApis = null;
    win = null;
}

function tryKillApiProcesses() {
    for (var api in launchedApis) {
        if (launchedApis.hasOwnProperty(api)) {
            try {
                launchedApis[api].process.kill();
            } catch (e) {
                console.error("Can't kill API process: " + e);
            }
        }
    };
}

If you now, in a command line, again execute:

ng serve
and, in another console:
electron .
You should still be able to see the electron window displaying the angular app but additionally when you open in a web browser http://localhost:8000/api/items and http://localhost:8001/api/shapes you should get the response from items and values APIs launched from Electron. 8000 and 8001 because Portfinder by default searches for a free port starting from 8000. If they are taken by something else then it could be 8002 or 8003 or so on. Also ShapesAPI could be hosted before Items API because portfinder callbacks work asynchronously.

You can find commit for this chapter under this link

Shared injectable angular service for retrieving API configuration from Electron

At last it’s time to get back to our angular project and make use of the services we have just hosted. But how angular website could know on which ports electron has hosted them? Well, the best way would be to just ask it. There is a way to communicate between the electron’s renderer process (our website) and the main process using IPC Modules. Let’s start by adding request handler to main.js:

ipcMain.on('GetPort', function (event, arg) {
    if (launchedApis.hasOwnProperty(arg))
        event.returnValue = launchedApis[arg].port;
    else
        console.error(arg + ' does not exist in electron exception');
});

This adds a handler to the "GetPort" message. Every time main thread responds synchronously with a port value of an API which name is passed as an argument. To respond synchronously the main thread has to put the response under the event.returnValue field.

On angular side we don’t want every service to ask separately for a port of all microservices it wants to use. Instead of that we should have one shared service which communicates with electron’s main thread and which we will inject to other services so we could ask for a port to each API only once and then serve it to all other curious services from its internal cache. To add such shared injectable service follow these steps:

  1. Add shared folder to src\app directory.
  2. In the shared folder execute the following command:
    ng generate service backend-endpoints
    This will generate new angular service called backed-endpoints.
  3. Include two generated files into the visual studio project.
  4. Open src\app\app.modules.ts file and at the top of the file add
    import { BackendEndpointsService } from './shared/backend-endpoints.service';
    and to the providers table add BackendEndpointsService. Thanks to that during runtime angular will create only one instance of this service and will inject it everywhere where needed.
Your app.module.ts should look like this:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { BackendEndpointsService } from './shared/backend-endpoints.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [BackendEndpointsService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Unfortunately now we need to stop for a while and do a small workaround. To invoke "GetPort" method using IPC we need to get a reference to the electron’s renderer thread. But currently it is not possible to require nor import electron module as it is being resolved at runtime so CLI (webpack) during compilation doesn’t know about electron and throws an exception. To workaround this issue we are going to follow the solution from electron.rocks site: http://electron.rocks/angular2-electron/. Add following code to the src/index.html file:

<script type="text/javascript">
  var electron = null;
  try {
    electron = require('electron');
  }
  catch (e) {
    console.log("Failed to require electron: " + e);
  }
</script>
And then the below line to backend-endpoint.service.ts:
declare var electron: any;

Before we can start implementing backend-endpoints service we need to do one more small thing. We need a place where we can store API names. We can also add there the default API endpoints configuration for case when the website is accessed via standard web browser. Thanks to that the page along with the APIs could be also hosted on a standard web server. All we would need to do is to change the configuration file.

In angular in src directory there is environments folder which stores configuration for different environments. You should have environment.prod.ts and environment.ts files inside. Add, after comma, the following entry to both:

apis: {
    itemsApi: {
        host: "http://localhost",
        port: 50842
    },
    shapesApi: {
        host: "http://localhost",
        port: 51012
    }
}
Port values should be the same as in the applicationUrl property in the Properties\launchSettings.json file of both API projects.

We can now import these settings to the service by adding the following line to its file:

import { environment } from "../../environments/environment";

My service implementation looks like this:

declare var electron: any;

import { Injectable } from '@angular/core';
import { environment } from "../../environments/environment";

@Injectable()
export class BackendEndpointsService {

    readonly ELECTRON_HOST: string = "http://localhost";
    readonly ELECTRON_METHOD_NAME: string = "GetPort";

    apis: { [name: string]: { host: string, port: number} } = {};
    
    constructor() {
        this.tryGetApisFromEnvironment();

        if (this.isElectronAvailable())
            this.setApisFromElectron();
    }

    private tryGetApisFromEnvironment() {
        if (environment && environment.apis)
            this.apis = environment.apis;
    }

    private isElectronAvailable(): boolean {
        return electron !== null && electron !== undefined;
    }

    private setApisFromElectron(): void {
        for (let apiName in this.apis) {
            if (this.apis.hasOwnProperty(apiName))
                this.setApiFromElectron(apiName);
        }
    }

    private setApiFromElectron(apiName: string) {
        let port = electron.ipcRenderer.sendSync(this.ELECTRON_METHOD_NAME, apiName);

        if (this.isInt(port)) {
            this.apis[apiName].host = this.ELECTRON_HOST;
            this.apis[apiName].port = parseInt(port);
        }
    }

    private isInt(value): boolean {
        return !isNaN(parseInt(value));
    }

    getApiEndpoint(apiName: string): string {
        if (!this.apis.hasOwnProperty(apiName))
            return "";

        let api = this.apis[apiName];

        return api.host + ":" + api.port + "/";
    }
}

At the very beginning, in the constructor, I retrieve the APIs configuration and assign it to the apis variable. Then after ensuring that the page is hosted in electron (electron variable is set in index.html) I ask, using IPC module communication, for a port of every API defined. Last method is a public one and takes an API name as a parameter and returns its URL address.

Commit with all the changes for this chapter can be found following this link

If you want to check if everything we did in this chapter work correctly then you can, for example, go to app.components.ts file and at the top add:

import { BackendEndpointsService } from './shared/backend-endpoints.service';
and a constructor inside class:
constructor(private backendEnpointsService: BackendEndpointsService) {
    this.title = this.backendEnpointsService.getApiEndpoint("shapesApi");
}

Now invoke as always in two separate command line tools:

ng serve
and
electron .

Additionally for more interesting result open http://localhost:4200 in your browser. You should see different ports there and in electron as shown in figure 8.

Angular application launched in electron and in standard webbrowser. Electron shows that API port is 8001 and web browser shows 51012.
Figure 8. Angualr application launched in Electron and in standard web browser

Revert changes done to app.components.ts before proceeding to the next chapter.

Invoke microservices from angular web app

There is not much left to add to our solution. The last thing is to add a new service which will utilize one of our microservices. Let’s say we will try to get all the shapes from ShapesAPI. So open a command line tool in the website’s root directory and execute:

ng generate service shapes

This will add a new shapes service to the src\app directory. In visual studio include two generated files in project and open shapes.service.ts. First thing is to modify the constructor to take two private variables as parameters. First will be our backend-endpoints service and the other one will be HttpClient. HttpClient is an angular’s overlay for XMLHttpRequest which is based on RxJS observables. You can read about it on angular guide site.

Inside the constructor we can ask the backend-endpoint service for the ShapesAPI endpoint. The only method in the service will be getShapes() which will return Observable to a result retrieved from the API. My file looks as follows:

import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Observable";

import { BackendEndpointsService } from './shared/backend-endpoints.service';

@Injectable()
export class ShapesService {

    serviceEndpoint: string = "";

    constructor(private backendEnpointsService: BackendEndpointsService, private http: HttpClient) {
        this.serviceEndpoint = this.backendEnpointsService.getApiEndpoint("shapesApi");
    }

    getShapes(): Observable {
        return this.http.get(this.serviceEndpoint + "api/shapes");
    }
}

To let angular know what it should inject into the http variable we need to add HttpClientModule to the app.module.ts’ imports table and import this module from '@angular/common/http'. After this modification file should look like this (highlited modified lines):

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { BackendEndpointsService } from './shared/backend-endpoints.service';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        AppRoutingModule,
        HttpClientModule
    ],
    providers: [BackendEndpointsService],
    bootstrap: [AppComponent]
})
export class AppModule { }

We can now go back to app.component.ts and make use of what we have just created. Import and add our shapes service to the constructor and to the component providers array. Also add a shapes string array variable to the class which will store retrieved shapes. We will get them from the service inside ngOnInit hook method which is called only once almost at the beginning of component’s lifecycle. For the lifecycle details visit the angular guide page. We need to import OnInit from ‘@angular/core’ and implement it in the AppComponent class.

New implementation is as follows:

import { Component, OnInit } from '@angular/core';
import { ShapesService } from './shapes.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [ShapesService]
})

export class AppComponent implements OnInit {
    title = 'app';
    shapes: string[] = [];

    constructor(private shapesService: ShapesService) { }

    ngOnInit(): void {
        this.shapesService.getShapes().subscribe(shapes => this.shapes = shapes);
    }
}

Let’s open app.component.html and iterate through the collection! Replace the h2 header and the list beneath it with:

<h2>Data returned from service: </h2>
<ul>
  <li *ngFor="let shape of shapes">
    <h3>{{shape}}</h3>
  </li>
</ul>

Commit for this chapter

Dealing with CORS

Actually we should be able to run our app now in a web browser or electron but most likely we will not get any shapes… Beautiful CORS error will make us to be unhappy people instead:

CORS error saying: Failed to load http://localhost:51012/api/shapes: No
Figure 9. CORS error when trying to access ShapesAPI

Fortunately there’s an easy way to fix that. In the API projects, we have to mirror all Origin header values coming from http://localhost to the Access-Control-Allow-Origin response header. To achieve this we have to add the following piece of code to the Startup’s configure method:

app.Use(async (context, next) =>
{
	if (context.Request.Headers.TryGetValue("Origin", out StringValues origins))
	{
		var localhostOrigins = origins.Where(origin => origin.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase));
		context.Response.Headers.Add("Access-Control-Allow-Origin", localhostOrigins.ToArray());
	}

	await next();
});
Add it before app.UseMvc(); instruction. Remember to add it to the both API projects.

Now, finally, we can compile our APIs and run the angular project in electron. As a result we should see what is in figure 10.

AngularWebsite run in Electron displaying data returned from ShapesAPI
Figure 10. AngularWebsite in Electron with data from ShapesAPI

Commit for this chapter.

What about tests?

Angular CLI’s new project comes with Jasmine test framework and Karma test runner. With every ng generate service command we executed to create a new service CLI also created a spec.ts file next to it. You can try and run below command in website’s folder to start Karma server and run the tests:

ng test

Unfortunately, as we haven’t followed TDD nor taken care about the existing ones, there are only 5 unit tests and all of them fail :)

Let’s fix this situation. As you can see most of the errors say that there is no provider for BackendEndpointsService:

Karma runner window with 5 tests failing with error message saying: "Failed: No provider for BackendEndpointsService!"
Figure 10. Karma runner page with 5 tests failing

These tests are broken because we added a new parameter to the app component and shapes service constructor about which the tests have no idea. Let’s start with app.component.spec.ts. We have three unit tests located in there. It is enough to import BackendEndpointsService and add it inside providers array after declarations:

import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { AppComponent } from './app.component';
import { BackendEndpointsService } from './shared/backend-endpoints.service';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      declarations: [
        AppComponent
      ],
      providers: [
          BackendEndpointsService
      ]
    }).compileComponents();
  }));

  it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));

  it(`should have as title 'app'`, async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('app');
  }));

  it('should render title in a h1 tag', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
  }));
});

Do the same for shapes.service.spec.ts. If you take a look at the unit tests output page you notice that now all the tests fail with "electron is not defined" error message. Well… it is hard to argue with that as we know that electron variable is declared in index.html file which Karma does not parse. As long as we are trying to build a website which could be run both in electron and in a regular web browser then our logic, except backend endpoints service of course, should be electron independent. So we can just mock this variable for Karma. To achieve this go to karma.conf.js and at the beginning of the object which is a parameter of config.set method, right before basePath property, add:

files: [
	'karma.globals.js'
],
This will make Karma to load karma.globals.js file. We need to create this file next to the configuration file. Its content is simply electron variable declaration:
electron = null;

The last thing to mock is the HttpClient. Angular contains special library which helps mocking backend responses. Let’s import HttpClientTestingModule and HttpTestingController to shapes.service.spec.ts and to app.component.spec.ts as shown below. The first one is necessary to redirect requests from HttpClient to the testing controller and the testing controller will be used to count requests and mock responses.

import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
We also need to add a module to imports array in the configuration for shapes.service.spec.ts:
beforeEach(() => {
    TestBed.configureTestingModule({
        providers: [ShapesService, BackendEndpointsService],
        imports: [HttpClientTestingModule]
    });
});
And app.component.spec.ts:
beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
          RouterTestingModule,
          HttpClientTestingModule
      ],
      declarations: [
        AppComponent
      ],
      providers: [
          BackendEndpointsService
      ]
    }).compileComponents();
  }));

Now kill the ng test task in the console (or close it and open again) and run this command again. All our tests should be green. But it would be a pity not to make any use of the testing controller. Let’s add the following test to app.component.spec.ts along with import for inject next to TestBed and async:

it('should retrieve all shapes', async(inject([HttpTestingController, BackendEndpointsService], (httpMock: HttpTestingController, backedEndpointService: BackendEndpointsService) => {
	const fixture = TestBed.createComponent(AppComponent);
	fixture.detectChanges();

	const shapesApiEndpoint = backedEndpointService.getApiEndpoint("shapesApi");

	const request = httpMock.expectOne(shapesApiEndpoint + "api/shapes");
	request.flush(["MockedRectangle", "MockedSquare"]);

	const app = fixture.debugElement.componentInstance;
	expect(app.shapes.length).toEqual(2)

	httpMock.verify();
})));

This test checks if the app component retrieves all the shapes in ngOnInit method. We inject HttpTestingController for mocking purpose and BackendEndpointsService so we could know which response to mock. In two first lines we create the component and let ngOnInit to be launched. Then ExpectOne method returns request object and tells the testing backed that we are expecting only one of this kind. Response mocking is done with flush method. Two last lines (not counting verify()) are the assertions. First checks if shapes variable in the app component stores exactly two mocks and the latter verifies if there was only one request.

Tests output now looks like in figure 11.

Karma runner window with 6 tests passing including AppComponent tests, ShapesService tests and BackendEndpointsService tests
Figure 11. Karma runner window with 6 tests passing

Commit for this chapter can be reviewed here.

Deployment

Finally we are here :)

The deployment step will be divided into two. First we will build production version of the webpage and then we will pack it into electron.

First step is, almost, as trivial as running the following command:

ng build –prod –base-href ./
This makes Angular CLI to build your application with optimizations and copy output to "dist" folder. Base-href option tells angular to set <base href/> value in index.html to "./" which is needed to properly run the website under electron. If you don’t want to remember about these parameters then you can add the following line to scripts section in package.json:
"build-electron": "ng build --prod --base-href ./",
Thanks to that you can run below command to build the application for electron:
npm run build-electron

If you wish the built web app to be hosted on a server then just skip base-href option. In such case all files from dist folder can be copied to any hosting server (e.g. IIS). My output looks like this:

dist folder containing 3rdpartylicenses.txt, favicon.ico, index.html, inline.bundle.js, main.bundle.js, polyfills.bundle.js, styles.bundles.css, vendor.bundle.js
Figure 12. Dist folder content

Attentive readers noticed that portError.html and apiError.html files are missing. Angular CLI, during the build process, turns all html template dependencies into javascript code and concatenates them into a bundle which speeds up the runtime. But this is not the case here as the mentioned files are electron dependencies not angular’s. We have two options here. The first one is to always remember to copy them manually after every build. Weak. The second one is to tell Angular that it has additional files it should care about. To do so we need to add these files to assets section of .angular-cli.json file:

"assets": [
  "assets",
  "favicon.ico",
  "apiError.html",
  "portError.html"
],
After building project again the missing files should appear in dist folder:
dist folder containing 3rdpartylicenses.txt, favicon.ico, index.html, inline.bundle.js, main.bundle.js, polyfills.bundle.js, styles.bundles.css, vendor.bundle.js, apiError.html, portError.html
Figure 13. Dist folder now also contains apiError.html and portError.html

Packing things into electron is only a bit more complicated. First of all we need to download appropriate electron prebuilt binaries version for our operating system from Electron releases page.

Latest available stable binaries for the time when writing this article is electron-v1.6.15-win32-x64.zip and this is what I’m going to use.

Go to the place where you extracted the prebuilt binaries and deeper to the resources folder. Create "app" folder here. It is a place where electron will look for your application to run it. Copy main.js and package.json to it from angular project directory. Let’s clear package.json a bit. We can remove scripts and devDepencies sections. Also all angular dependencies can be removed as they are in the bundle produced by CLI. The file content should look similar to this:

{
  "name": "angular-website",
  "version": "0.1.0",
  "main": "main.js",
  "license": "MIT",
  "private": true,
  "dependencies": {
    "core-js": "^2.4.1",
    "dotenv": "^4.0.0",
    "portfinder": "^1.0.13",
    "rxjs": "^5.4.1",
    "zone.js": "^0.8.14"
  }
}

We need to restore these dependencies here so run the following command and wait for npm to do the job:

npm install

Compile ShapesApi and ItemsApi in the release configuration and copy the compiled dlls together with *.runtimeconfig.json and *.deps.json files next to main.js and package.json. Your resources\app folder should look like in figure 14:

Content of electron/resources/app: node_modules folder, itemsApi.deps.json, ItemsApi.dll, ItemsApi.runtimeconfig.json, main.js, package.json, ShapesApi.deps.json, ShapesApi.dll, ShapesApi.runtimeconfig.json
Figure 14. Content of electron/resources/app folder

Now create "src" folder here and copy all files generated by CLI’s build command to it:

Electron/resources/app/src folder containing 3rdpartylicenses.txt, favicon.ico, index.html, inline.bundle.js, main.bundle.js, polyfills.bundle.js, styles.bundles.css, vendor.bundle.js, apiError.html, portError.html
Figure 15. Content of electron/resources/app/src folder

Ok. The last thing to do is the .env file. Copy it to the root path of extracted electron binaries (to the place where electron.exe is located). We have to adjust the .env variables to match our production needs. Edit it and change PRODUCTION flag to true. This will make electron to load index.html rather than displaying localhost:4200 url content (this is how we coded main.js file). Next thing is to change filePath properties in APIS line to point at dlls located in ".\\resources\\app":

PRODUCTION=true
APIS=[{"name":"itemsApi","filePath":".\\resources\\app\\ItemsApi.dll"},{"name":"shapesApi","filePath":".\\resources\\app\\ShapesApi.dll"}]
APPLICATION_URL=http://localhost:4200
APPLICATION_FILE_PATH=src\index.html
APPLICATION_PORT_ERROR_FILE_PATH=src\portError.html
APPLICATION_API_ERROR_FILE_PATH=src\apiError.html
WINDOW_DEFAULT_WIDTH=1024
WINDOW_DEFAULT_HEIGHT=768

Save and close. Now is the great moment. Rename electron.exe to something else like AngularCoreMicroservicesElectron.exe and launch it. Voilà:

Screenshot showing AngularWebsite standalone run in Windows
Figure 16. AngularWebsite standalone run in Windows

Your application is ready for distribution! It was a long road but, in my opinion, worth it. Just look how cool ".NET desktop app" looks under Ubuntu :)

Screenshot showing AngularWebsite standalone run in Ubuntu
Figure 17. AngularWebsite standalone run in Ubuntu

Smart move would be to prepare a deployment powershell script which will do the entire job for us. Also if, for some reason, nothing happens after launching the application then you can try running it from a console with --enable-logging option. This turns logging all errors to the console on which should help investigating the issue.

Changes done in this chapter can be found here.

Ready to run application can be downloaded from here: AngularCoreMicroservicesElectron Windows application.

Linux tweaks

It is not completely like that the app works perfectly under Linux without changing anything :)

The first thing, except that electron prebuilt binaries have to be one that targets Linux, is that all double backslashes in .env config file has to be changed to single slashes.

Second thing is that .NET starting time on Linux is a bit longer than on Windows. At least on my machine. And because of that ngOnInit’s API request hits the wall. To solve that I decided to add Status controller to the APIs and wait with loading index.html until all Status responses are OK.

New controller in the APIs:

using Microsoft.AspNetCore.Mvc;

namespace ItemsApi.Controllers
{
    [Route("api/[controller]")]
    public class StatusController : Controller
    {
        [HttpGet]
        public string Get()
        {
            return "OK";
        }
    }
}

Main.js changes:

//[…]
	
const ApiStatusesChecker = require('./apiStatusesChecker');
const API_STATUS_CHECK_MAX_RETRIES = 5;
const API_STATUS_CHECK_INTERVAL_IN_MS = 1000;
let apiStatusTests = 0;

//[…]

//This method has already existed. Only loadContent() invoke has been replaced by waitAllApisStatusOKAndLoadContent()
function startApiAndLoadContent(api, port) {
    if (win === null)
        return;

    startApi(api, port);

    if (areAllApisStarted())
        waitAllApisStatusOKAndLoadContent();
}

function waitAllApisStatusOKAndLoadContent() {
    if (apiStatusTests == API_STATUS_CHECK_MAX_RETRIES) {
        handleApiLoadingError(`Not all APIs returned OK status after ${apiStatusTests} retries`);
        return;
    }

    apiStatusTests++;

    new ApiStatusesChecker(launchedApis)
        .checkAllApisStatusOK()
        .then(loadContent)
        .catch(invokeWaitAllApisStatusOKWithTimeout);
}

function invokeWaitAllApisStatusOKWithTimeout() {
    setTimeout(waitAllApisStatusOKAndLoadContent, API_STATUS_CHECK_INTERVAL_IN_MS);
}

//[…]

New file (apiStatusesChecker.js) in the web project root folder:

const http = require('http');

module.exports = function (launchedApis) {
    var self = this;

    var apiNames = Object.keys(launchedApis);
    var responsesCount = 0;
    var okResponsesCount = 0;

    var promiseResolve = null;
    var promiseReject = null;

    this.checkAllApisStatusOK = function () {
        return new Promise(function (resolve, reject) {
            promiseResolve = resolve;
            promiseReject = reject;

            apiNames.forEach(checkApiStatus);
        });
    }

    function checkApiStatus(apiName) {
        let requestUrl = `http://localhost:${launchedApis[apiName].port}/api/status`;

        http.get(requestUrl, handleResponse).on("error", handleRequestError);
    }

    function handleResponse(response) {
        if (response.statusCode !== 200) {
            responsesCount++;
            checkResponsesAndResolvePromise();
            return;
        }

        let responseData = "";

        response.on("data", function (chunk) { responseData += chunk });
        response.on("end", function () {
            responsesCount++;

            if (responseData === "OK")
                okResponsesCount++;

            checkResponsesAndResolvePromise();
        });
    }

    function handleRequestError(error) {
        responsesCount++;

        checkResponsesAndResolvePromise();
    }

    function checkResponsesAndResolvePromise() {
        if (responsesCount === apiNames.length) {
            if (responsesCount === okResponsesCount)
                promiseResolve();
            else
                promiseReject();
        }
    }
}

With these changes you can again go through “Deployment” chapter and prepare Linux distributable version of our app :) Only remember to copy apiStatusesChecker.js together with main.js now!

GitHub link for changes done in this chapter

Ready to run application for Linux can be downloaded from here: AngularCoreMicroservicesElectron Linux application.

Enjoy!