Self-hosting an Angular 2 website in a windows service

Lately I've really wanted to explore the possibilities of self-hosting an Angular 2 website within a Windows service that could be running on any arbitrary windows machine. This is somewhat inspired by applications such as Seq where very powerful functionality is hosted in a simple windows service. I also use Octopus Deploy regularly to ease deployments of ASP.NET websites, and I'd like to start using it for services.

In my specific case I am eventually looking to wrap access to physical hardware, that could be connected via USB or serial, behind a WebAPI and provide an Angular 2 site for configuration and diagnostics. This post is an attempt to get started on this architecture, and see what it takes to pull it off. We'll go through the following steps:

  1. Get a windows service implemented using Topshelf
  2. Add an ASP.NET WebAPI website to the service using OWIN to host everything
  3. Add an Angular 2 website using Nancy, SystemJS & CDN links
  4. Make an API call within Angular2 to show it all working together
  5. Install the service so it'll run within Windows

In a future post I'll dive into using Webpack to build our Angular 2 code into static files we can deploy with the service. This would provide a "truly" self-hosted solution.

The code for all this is hosted in my Github repo: https://github.com/sstorie/experiments/tree/master/topshelf-angular2-service

Ready? Let's get started!

Step 1 - Creating the windows service

To build the windows service I'm going to leverage the open source Topshelf framework. This framework simplifies the work required to get an application hosted within a service. I'll walk through the basic steps here, but the project has really nice docs available:

https://topshelf.readthedocs.org/en/latest/index.html

To get started simply create a console app, and import the Topshelf nuget package:

PM> Install-Package Topshelf  

Once you have the package installed we're ready to make a very simple service using the provided HostFactory static factory. To do this we just need to define the class that will perform whatever work is required while the service is running. We'll call this class the ServiceHost:

using System;

namespace Service  
{
    /// <summary>
    /// The class that provides the actions to perform when starting/stopping the service.
    /// </summary>
    public class ServiceHost
    {
        public ServiceHost()
        {
            Console.WriteLine("ServiceHost constructed");
        }

        public void Start()
        {
            Console.WriteLine("ServiceHost started");
        }

        public void Shutdown()
        {
            Console.WriteLine("ServiceHost shutting down");
        }

        public void Stop()
        {
            Console.WriteLine("ServiceHost stopped");
        }
    }
}

As we're just trying to get the service running, this doesn't do anything interesting yet, but we'll change that soon enough. The other piece we need is to incorporate Topshelf so this class is used to create an actual Windows service. This is taken mostly from the docs, but it's quite simple to do:

using Topshelf;

namespace Service  
{
    class Program
    {
        static void Main(string[] args)
        {
            HostFactory.Run(factory =>
            {
                // Provide the service's behavior using our custom
                //  ServiceHost class
                //
                factory.Service<ServiceHost>(service =>
                {
                    service.ConstructUsing(name => new ServiceHost());
                    service.WhenStarted(sh => sh.Start());
                    service.WhenShutdown(sh => sh.Shutdown());
                    service.WhenStopped(sh => sh.Stop());
                });

                // Now define some attributes of the service overall
                //
                factory.RunAsLocalSystem();

                // Provide the metadata to the service control
                //
                factory.SetServiceName("self-hosted-angular2-service");
                factory.SetDisplayName("Self-hosted Angular 2 service");
                factory.SetDescription("A custom service that hosts an Angular 2 website using OWIN");

            });
        }
    }
}

Once you have this code in place, you can run the solution within Visual Studio and see topshelf in action:

topshelf basic service working

We can see our console message displayed, so we're good to start adding some more useful functionality.

Step 2 - Add an OWIN hosted ASP.NET website

To get started on our self-hosted API we need to add a new project to the solution that will contain all the API (and eventually website) code. Add a new DLL project called Service.Website to the solution.

Then add in the Microsoft.AspNet.WebApi.OwinSelfHost nuget package to bring in what we need to self-host WebAPI:

install-package Microsoft.AspNet.WebApi.OwinSelfHost  

You also probably want to update the installed packages now to get the latest versions (depending on your Nuget settings). You'll also need to add a Nuget reference in the Service project to the Microsoft.Owin.Host.HttpListener package, otherwise you'll get an exception when trying to run the service (once we add the code below).

Now with these required libs in place we can build out a simple WebAPI using the following steps:

  1. Move the ServiceHost class into the Service.Website project
  2. Add an OWIN Startup class to configure the API
  3. Add a simple ApiController to provide some functionality
  4. Update the ServiceHost class to create and run our OWIN server when Topshelf starts our service.

Moving the ServiceHost class is easy enough, so we'll show that in a moment with the updated code. The OWIN startup class is taken almost exactly from the docs, but we make two changes. First, I am mapping the API code the /api instead of the root url so we leave that open for the eventual website we're adding. I also prefer attribute routing over convention-based routing, so I'm using that. Here's what Startup.cs looks like:

using System.Web.Http;  
using Owin;

namespace Service.Website  
{
    class Startup
    {
        // This code configures Web API. The Startup class is specified as a type
        // parameter in the WebApp.Start method.
        //
        public void Configuration(IAppBuilder appBuilder)
        {
            // We're going to hang the web API off off the /api "sub"-url so that we
            //  leave the root url open for the Angular 2 website.
            //
            appBuilder.Map("/api", api =>
            {
                // Create our config object we'll use to configure the API
                //
                var config = new HttpConfiguration();

                // Use attribute routing
                //
                config.MapHttpAttributeRoutes();

                // Now add in the WebAPI middleware
                //
                api.UseWebApi(config);
            });
        }
    }
}

With that we can then create our very simple ApiController that will just return the current date/time when a GET request is made to /api/time:

using System;  
using System.Web.Http;

namespace Service.Website.Api  
{
    [RoutePrefix("time")]
    public class TimeController : ApiController
    {

        [Route("")]
        public IHttpActionResult Get()
        {
            return Ok(DateTimeOffset.Now);
        }
    }
}

The final piece to complete this is to update our ServiceHost class to actually run the OWIN server when Topshelf calls our startup method. Here's the updated code:

using System;  
using Microsoft.Owin.Hosting;

namespace Service.Website  
{
    /// <summary>
    /// The class that provides the actions to perform when starting/stopping the service.
    /// </summary>
    public class ServiceHost
    { 
        // Create the variable for our self-hosted server
        //
        private IDisposable _server;

        // Just hard-code the address for now
        //
        private string _baseAddress = "http://localhost:7331";

        public ServiceHost()
        {
            Console.WriteLine("ServiceHost constructed");
        }

        public void Start()
        {
            Console.WriteLine("ServiceHost started");

            // Start up the server by providing our OWIN Startup class as the source type.
            //  We also save the return object so we can dispose of it properly when the
            //  service is shutdown
            //
            _server = WebApp.Start<Startup>(url: _baseAddress);

            Console.WriteLine($"Server running at {_baseAddress}");
        }

        public void Shutdown()
        {
            Console.WriteLine("ServiceHost shutting down");

        }

        public void Stop()
        {
            Console.WriteLine("Server shutting down");

            // Dispose of the server object since we're shutting everything down
            //
            _server.Dispose();

            Console.WriteLine("ServiceHost stopped");
        }
    }
}

Once we have this new code in place your solution should look like this:

and when you run the project you should see the following output:

Now you can use any REST client to try hitting the /api/time url and the current time should be returned. This image is a shot of Postman, but any client will do:

Step 3 - Add an Angular 2 site (using Nancy and CDN-served files)

Now that we have the API in place and Topshelf is hosting everything for us, let's build upon our OWIN pipeline to host the actual Angular 2 site itself. To keep this simple we'll first use Nancy to serve a page that will load Angular 2 using SystemJS and CDN links.

I am using Nancy here just because I'm a bit more familiar with it than traditional MVC, and it's just so simple to get something up and running. To add our website we just need to do the following:

  1. Add in the Nancy.Owin nuget package to pull in Nancy and the Owin-related dlls
  2. Update our Startup.cs file to include Nancy in the OWIN pipeline
  3. Add a Nancy module to provide a default web page
  4. Add a simple view location convention to Nancy so we can use our own folder structure
  5. Add some Angular 2 typescript code that will be rendered in the browser

So pull the Nancy.Owin nuget package into the Service.Website project. Once that's there we need to update the Startup class to include the following (this is not the entire file...just the updated bit):

                // Now add in the WebAPI middleware
                //
                api.UseWebApi(config);
            });

            // Add Nancy to the OWIN pipeline.
            //  Note, because this is registered last we don't need to worry 
            //  about falling-through to any other middleware. Any requests to
            //  /api/... will be handled by WebAPI first, and anything else
            //  will fall through to Nancy
            //
            appBuilder.UseNancy();
        }

Once that code is in place let's create our Nancy module using the following folder structure:

In many cases you might not need a custom Bootstrapper when using Nancy, but I like to keep my modules contained, and since we're using a top-level Website/ folder we need one. However, it's quite simple:

using Nancy;  
using Nancy.TinyIoc;

namespace Service.Website.Website.Configuration  
{
    public class Bootstrapper : DefaultNancyBootstrapper
    {
        protected override void ApplicationStartup(TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines)
        {
            // Add a view location convention that looks for views in a folder
            //  named "views" next to the module class
            //
            this.Conventions.ViewLocationConventions.Add((viewName, model, context) => $"Website/Modules/{context.ModuleName}/views/{viewName}");
        }
    }
}

Now the module itself is even simpler:

using Nancy;

namespace Service.Website.Website.Modules.Root  
{
    public class RootModule : NancyModule
    {
        public RootModule()
        {
            // Define a single route that returns our index.html view
            //
            Get["/"] = _ => View["index"];
        }
    }
}

...and just for completeness here's all we have for index.html:

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">  
<head>  
    <meta charset="utf-8" />
    <title></title>
</head>  
<body>  
    Nancy is working!
</body>  
</html>  

Make sure you specify that the index.html file should always be copied to the output directory too (thanks to @broAhmed for the feedback). With this new code in place we have Nancy working, so if you fire up the project you can now browse to http://localhost:7331 and see the following:

You should also be able to make the API call just like before and see that it still works as well.

The final piece of this step is to add the Angular 2 code we need. To get started we'll update the index.html file to match what's used within the Angular 2 docs:

<!DOCTYPE html>  
<html>  
<head>  
    <title>Angular 2 QuickStart</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Don't worry about styles for now -->
    <!--<link rel="stylesheet" href="styles.css">-->

    <!-- 1. Load libraries -->
    <!-- IE required polyfills, in this exact order -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.0/es6-shim.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.20/system-polyfills.js"></script>
    <script src="https://npmcdn.com/angular2@2.0.0-beta.13/es6/dev/src/testing/shims_for_IE.js"></script>

    <script src="https://code.angularjs.org/2.0.0-beta.13/angular2-polyfills.js"></script>
    <script src="https://code.angularjs.org/tools/system.js"></script>
    <script src="https://npmcdn.com/typescript@1.8.9/lib/typescript.js"></script>
    <script src="https://code.angularjs.org/2.0.0-beta.13/Rx.js"></script>
    <script src="https://code.angularjs.org/2.0.0-beta.13/angular2.dev.js"></script>
    <!-- 2. Configure SystemJS -->
    <script>
        System.config({
            transpiler: 'typescript',
            typescriptOptions: { emitDecoratorMetadata: true },
            packages: { 'app': { defaultExtension: 'ts' } }
        });
        System.import('app/main')
              .then(null, console.error.bind(console));
    </script>
</head>  
<!-- 3. Display the application -->  
<body>  
    <my-app>Loading...</my-app>
</body>  
</html>

<!--  
Copyright 2016 Google Inc. All Rights Reserved.  
Use of this source code is governed by an MIT-style license that  
can be found in the LICENSE file at http://angular.io/license  
-->

Now, to serve the the typescript files up as static content we need to update Nancy to treat them as such. We also want to tell Visual Studio that it shouldn't compile the typescript files down, since we're using SystemJS to do so (for the moment). With the following update to our folder structure:

We need to update the properties of each html and typescript file to be treated as content that should be copied to the build folder every time a build happens:

The content of the angular files are the same as the Google example, so I'm not showing them, but here's the updated Nancy bootstrapper that tells Nancy to serve up the App folder as static content:

        protected override void ApplicationStartup(TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines)
        {
            // Add a view location convention that looks for views in a folder
            //  named "views" next to the module class
            //
            this.Conventions.ViewLocationConventions.Add((viewName, model, context) => $"Website/Modules/{context.ModuleName}/views/{viewName}");

            // Add a new path for static content so our typescript files located in
            //  the 'App' folder can be served to SystemJS
            //
            this.Conventions.StaticContentsConventions.Add(StaticContentConventionBuilder.AddDirectory("App"));

Now with all of these changes in place we're ready to test it out. Hopefully, when you run the project now you can navigate again to the URL and see the following:

Step 4 - Update Angular to make an API call

This step is pretty much vanilla Angular 2 code, but let's just show that we can infact interact with our WebAPI from the new Angular 2 website. Here I'm just going to update the App component so make a call using the HTTP provider from Angular. The first thing we'll do is update the template to include a button that'll make the API request when clicked, a new label that'll show the result, and of course the component code to support both of these:

import {Component} from 'angular2/core';  
import {Http, Response, HTTP_PROVIDERS} from "angular2/http";

@Component({
    selector: 'my-app',
    template: `
        <h1>Making an API call</h1>
        <button (click)="callApi()">Get Current Time</button>    
        <div>
            Here's the current time: {{currentTime}}
        </div>
    `,
    providers: [HTTP_PROVIDERS]
})
export class AppComponent {

    /**
     * The current time returned from the API
     */
    currentTime: string;

    /**
     * Makes a call to the self-hosted API to get the current time
     */
    callApi(): void {
        this.http.get("/api/time")
            .subscribe(
            (response: Response) => {
                this.currentTime = response.json();
            });
    }


    constructor(
        private http: Http
    )
    { }
}

/*
Copyright 2016 Google Inc. All Rights Reserved.  
Use of this source code is governed by an MIT-style license that  
can be found in the LICENSE file at http://angular.io/license  
*/

Now you might have noticed that I'm including some new HTTP stuff in here. This is not provided by the angular 2 core package, so we need to update our index.html to pull in the http code as well:

    <script src="https://code.angularjs.org/2.0.0-beta.13/Rx.js"></script>
    <script src="https://code.angularjs.org/2.0.0-beta.13/angular2.dev.js"></script>
    <script src="https://code.angularjs.org/2.0.0-beta.13/http.dev.js"></script>
    <!-- 2. Configure SystemJS -->
    <script>

With this updated code in place however, we can now run the app and you should see it working like this:

Note, if you're following along and things aren't updating when you build, you likely need to do a rebuild each time, and not just a simple build. I find Visual Studio skips projects when it doesn't think anything changed, and since we're not hosting this in IIS we can't just update the static files while it's running and see the updates.

Step 5 - Installing the service

If you're still with me, then thank you for reading this far :) The last step in our exploration is getting the actual service installed and running by itself in Windows. Now, while this post may be long, it's hopefully been a series of very simple things with some explanations, and this last step is no different. When you build a Topshelf project it includes everything you need to install the service with with your compiled project. let's see what I mean.

I'm gonna switch the build to release mode, and compile it, and open a shell window to the release directory:

From there installing the service is literally this simple:

./service.exe install --autostart

What's happening is Topshelf included everything we need right in our own executable (service.exe). We just pass the parameter to install the service, and I included an option to start the service automatically when windows starts. When you run that you should see output like this:

C:\code\github\experiments\topshelf-angular2-service\Service\bin\Release [master +0 ~2 -0]> ./service.exe install --autostart

Configuration Result:  
[Success] Name self-hosted-angular2-service
[Success] DisplayName Self-hosted Angular 2 service
[Success] Description A custom service that hosts an Angular 2 website using OWIN
[Success] ServiceName self-hosted-angular2-service
Topshelf v3.3.154.0, .NET Framework v4.0.30319.42000

Running a transacted installation.

Beginning the Install phase of the installation.  
Installing Self-hosted Angular 2 service service  
Installing service self-hosted-angular2-service...  
Service self-hosted-angular2-service has been successfully installed.

The Install phase completed successfully, and the Commit phase is beginning.

The Commit phase completed successfully.

The transacted install has completed.  

So now your service is installed, but it's not running yet. To get it running just open the service control manager, and start the service like any other:

Then you should be able to navigate to the URL and see your site running! Easy right?!

Wrapping up

So in this post I showed how simple it is to use a windows service to self-host an Angular 2 website with Web API. We walked through the process of using Topshelf to build the service itself, adding WebAPI using OWIN, and finally adding Nancy to host the website content and serve the Angular 2 typescript files. Again, the code for all this is available in my Github repo:

https://github.com/sstorie/experiments/tree/master/topshelf-angular2-service

In a future post I'll walk through how to add Webpack to compile our typescript into truly static files we can serve without needing any external CDNs.

Please let me know if you have any feedback or comments!

comments powered by Disqus