Diagnose ASP.Net Core startup issues when hosted as a Windows Service

One of the great features released in ASP.Net Core 2.1 is the capability to host a application as a Windows Service. For Windows based production environments, Windows Services tend to be the chosen execution strategy for smaller services. It's a bare bones solution compared to IIS, but in a lot of cases, developers just need an open port to interact with their application.

Another core feature of .Net Core is the capability of compiling your application as a self-contained application. This means that you don't need to install .Net on the machine the application is intended to run on. The generated output will contain all the necessary files to run as a stand-alone application.

Another feature we need to praise .Net Core for is how easy it is to install. And why this is relevant you may ask? If your Continuous Integration pipeline supports running PowerShell or shell scripts, follow the guide described in this page to learn how to install it. Then if you combine the two previously mentioned features (run as a Windows Service and compile as a self-contained application), existing Windows Services dependent on full .Net framework can be swapped with .Net Core application with minimal effort (assuming the same naming convention are kept).

# installs .Net Core SDK 2.1.400, if it was not previously installed
if ((dotnet --version | Where-Object { $_ -match '^2.1.400$' } | Measure-object).Count -ne 1) {
    Write-Output 'Installing latest version of .Net Core (2.1.400)'
    Invoke-WebRequest 'https://dot.net/v1/dotnet-install.ps1' -o dotnet-install.ps1
    .\dotnet-install.ps1 -Channel 2.0 -Version 2.1.400 -InstallDir 'C:/Program Files/dotnet/'
}

dotnet --version

However, the main focus on this article is on how to troubleshoot Windows Service startup issues, in a context of a given .Net Core application, when you don't have access to the target machine. This was something I faced recently, so I thought is was worth sharing the PowerShell scripts I used.

Let's create a example application from scratch. The following code snippets were created with the assumption PowerShell 5.1 and .Net Core SDK 2.1.400 are installed.

mkdir example_app; cd example_app
dotnet new web
dotnet add package Microsoft.AspNetCore.Hosting.WindowsServices --version 2.1.1
# edit Program.cs to enable host to run as service
dotnet publish --self-contained --runtime win7-x64 --output dist

The application is now compiled. Let's run it as a Windows Service.

New-Service -Name example-service -BinaryPathName ((Get-Item .\dist\example-app.exe).FullName)
Start-Service -Name example-service
Get-Service -Name example-service
Invoke-WebRequest 'http://localhost:5000' -UseBasicParsing

Everything should be working without issues. Now, for experimentation purposes, let's throw an exception. Open Program.cs in your favourite editor and copy-paste the following code.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.WindowsServices;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace example_app
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().RunAsService();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args)
        {
            var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
            var pathToContentRoot = Path.GetDirectoryName(pathToExe);

            // let's make it crash
            throw new Exception("forcing fatal exception...");

            return WebHost.CreateDefaultBuilder(args)
                .UseContentRoot(pathToContentRoot)
                .UseStartup<Startup>();
        }
    }
}

The service previously created should still be running. Let's stop, delete, compile, install and start our application (now throwing an exception).

Stop-Service -Name example-service
sc.exe delete example-service
Remove-Item './dist/' -Force -Recurse
dotnet publish --self-contained --runtime win7-x64 --output dist
New-Service -Name example-service -BinaryPathName ((Get-Item .\dist\example-app.exe).FullName)
Start-Service -Name example-service

Now let's take a look at Event Viewer logs.

$applicationName = 'example-app'
Get-EventLog -Newest 10 -LogName System -Source "Service Control Manager" | ? { $_.Message -match $applicationName }
Get-EventLog -Newest 10 -LogName Application -Source @("Application Error", ".Net Runtime", "Windows Error Reporting") | ? { $_.Message -match $applicationName }

That's it. You should be able to find the root cause of the issues in the message of the outputted logs.

Finally, this is only an example so let's clean up what we have made. Notice the last step uses C:\Windows\System32\sc.exe instead of Remove-Service. The reason is Remove-Service is a PowerShell 6 cmdlet and Windows 10 ships with PowerShell 5.1. Just to be safe, I prefer to call the Service Control executable.

Stop-Service -Name example-service
sc.exe delete example-service

References