Running a .NET Core Generic Host App as a Windows Service

I created a basic .NET Console application, mistreatment the Generic Host pattern and took a glance to check if the RunAsService extension, mentioned within the ASP.NET Core documentation, would work. Turns out, it doesn’t, as it’s not on the market on the IHostBuilder interface, just for the IWebHostBuilder. Next, I believed I’d take a glance at the ASP.NET Core Hosting repository to check if I might find out if this was on the market beneath a distinct name or failing that to check if there have been any plans for similar practicality for IHostBuilder.

My next steps were then supported the ASP.NET Core documentation. However, as that document is particular to ASP.NET Core, I believed it’d be valuable to summarise the steps needed for running a generic host .NET core app as a Windows service here on my journal. I won’t cowl everything in the maximum amount detail because the full documentation that I counsel you furthermore may go and skim for extra context and clarification.

NOTE: an entire sample for a basic generic host based mostly .NET Core Windows service are often found on my GitHub repository.

Project File

After creating a new .NET Core console application, the first thing we need to do is to add a reference to the Microsoft.Extensions.Hosting package and the Microsoft.AspNetCore.Hosting.WindowsServices System.ServiceProcess.ServiceController package.

 <Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <RuntimeIdentifier>win7-x64</RuntimeIdentifier>
    <LangVersion>7.1</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.ServiceProcess.ServiceController" Version="4.5.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="2.1.1" />
  </ItemGroup>

</Project>

As well as these package reference, you’ll see that in the main PropertyGroup section we’ve also set the RuntimeIdentifier to win7-x64. The RID (Runtime Identifier) in this case identifies that the target platform for this application is Windows. Since Windows Services are only available on Windows, it makes sense to set a windows target platform which will also ensure we produce a .EXE rather than .DLL file when building our application.

In the above sample we can also set the LangVersion to 7.1 to allow me to use an async Main method.

Program.cs

Next we need to setup our application entry point in Program.cs. The Main method is our entry point and in my sample is as follows:

internal class Program
{
    private static async Task Main(string[] args)
    {
        var isService = !(Debugger.IsAttached || args.Contains("--console"));

        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<FileWriterService>();
            });

        if (isService)
        {
            await builder.RunAsServiceAsync();
        }
        else
        {
            await builder.RunConsoleAsync();
        }
    }
}

I’ve based this example on the “Host ASP.NET Core in a Windows Service” documentation.

First, it performs a check to see if we’re either debugging or if the application has been started having been passed an argument of “–console”. In that case, we set the is Service flag as false. We’ll use this later to allow us to debug the application from Visual Studio as a standard console application.

Next, we create a HostBuilder which we’ll use to create the generic host. In here we register one service which is an implementation of the IHostedService interface. This is how we’ll run code within our application. We’ll look at the implementation for this application a little further down.

Finally, using the is Service flag, we now call either the RunAsServiceAsync extension on the builder or the RunConsoleAsync. The later will run the application as a normal .NET Core console app, perfect for local testing and debugging. The former is the extension method I was missing earlier. This comes from two files I have added, copied from the GenericHostSample mentioned earlier.

ServiceBaseLifetime.cs

// Code from https://github.com/aspnet/Hosting/blob/2a98db6a73512b8e36f55a1e6678461c34f4cc4d/samples/GenericHostSample/ServiceBaseLifetime.cs

public class ServiceBaseLifetime : ServiceBase, IHostLifetime
{
private readonly TaskCompletionSource<object> _delayStart = new TaskCompletionSource<object>();

public ServiceBaseLifetime(IApplicationLifetime applicationLifetime)
{
ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
}

private IApplicationLifetime ApplicationLifetime { get; }

public Task WaitForStartAsync(CancellationToken cancellationToken)
{
cancellationToken.Register(() => _delayStart.TrySetCanceled());
ApplicationLifetime.ApplicationStopping.Register(Stop);

new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing.
return _delayStart.Task;
}

private void Run()
{
try
{
Run(this); // This blocks until the service is stopped.
_delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
}
catch (Exception ex)
{
_delayStart.TrySetException(ex);
}
}

public Task StopAsync(CancellationToken cancellationToken)
{
Stop();
return Task.CompletedTask;
}

// Called by base.Run when the service is ready to start.
protected override void OnStart(string[] args)
{
_delayStart.TrySetResult(null);
base.OnStart(args);
}

// Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync.
// That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion.
protected override void OnStop()
{
ApplicationLifetime.StopApplication();
base.OnStop();
}
}

I won’t go into the internals of this too deeply. This is entirely copied from the Microsoft sample. This class derives from the ServiceBase class used to define Windows services. It also implements the IHostLifetime interface from Microsoft.Extensions.Hosting.

ServiceBaseLifetimeHostExtensions.cs

// Code from https://github.com/aspnet/Hosting/blob/2a98db6a73512b8e36f55a1e6678461c34f4cc4d/samples/GenericHostSample/ServiceBaseLifetime.cs

public static class ServiceBaseLifetimeHostExtensions
{
public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
}

public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
{
return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
}
}

This class is also copied from the GenericHostSample and adds the RunAsServiceAsync extension method which will ensure the ServiceBaseLifetime is registered as the implementation for IHostLifetime.

FileWriterService

public class FileWriterService : IHostedService, IDisposable
{
private const string Path = @"d:\TestApplication.txt";

private Timer _timer;

public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(
(e) => WriteTimeToFile(),
null,
TimeSpan.Zero,
TimeSpan.FromMinutes(1));

return Task.CompletedTask;
}

public void WriteTimeToFile()
{
if (!File.Exists(Path))
{
using (var sw = File.CreateText(Path))
{
sw.WriteLine(DateTime.UtcNow.ToString("O"));
}
}
else
{
using (var sw = File.AppendText(Path))
{
sw.WriteLine(DateTime.UtcNow.ToString("O"));
}
}
}

public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);

return Task.CompletedTask;
}

public void Dispose()
{
_timer?.Dispose();
}
}

This class implements IHostedService and once registered will be started and stopped by the IHost. It will run the code in WriteTimeToFile method which appends the current time to a file. Not particularly useful, but for this example, it’s enough to show that something is actually happening.

At this point, we now have our complete sample application. We can run this directly in Visual Studio to test it, but our main goal is to register this as a Windows service.

Again, if you want to download the source yourself you can do so from GitHub.

Creating the Windows Service

The final step is to build our code and register a service from the executable. We’ll follow the ASP.NET Core documentation to build our code and register a Windows service. Again, I won’t cover the detail here since the referenced documentation provides ample explanation. To summarise though, the steps are as follows:

Build and publish the project from the command line. This command can be run in the directory in which the project resides…

dotnet publish --configuration Release

Next, use sc.exe to create a service, passing the full path of the built executable…

sc create MyFileService binPath= "E:\Projects\IHostedServiceAsAService\IHostedServiceAsAService\bin\Release\netcoreapp2.1\win7-x64\publish\IHostedServiceAsAService.exe"

Next, use sc.exe to start the service (this needs to occur in a command prompt running as Administrator)…

sc start MyService

At this point, our service is running. In my case, I confirmed that a text file called TestApplication.txt appeared in my D: drive (that destination and filename is hardcoded in my sample), which confirms that the service is running as expected. Every minute, while the service is running, I can see a new line added to the text file.

Summary

If you’ve followed on, you must currently have a .NET Core primarily based Windows service running on your machine. As per the need within the original question, you’ll simply modify this service to scan from a queue and method the messages it receives. Personally, I tend to deploy such services as a basic console application, running in UNIX primarily based loader containers. For production workloads, this permits simple scaling and that I will manage the services through our instrumentation orchestration in AWS ECS. However, must you want a Windows service, then hopefully this post is enough to induce you started.

Hopefully, during a future unleash of ASP.NET Core, this practicality is enclosed within the framework. If that happens we have a tendency to won’t embody our own ServiceBaseLifetime and ServiceBaseLifetimeHostExtensions categories. supported a reply from Chris within the original GitHub issue, that appears like one thing which can be thought of within the three.0 timeframe as a part of some wider hosting work they decide to do.

Looking for .NET Core and .NET Framework hosting provider?

You should really consider finding an option that offers both features and transparent pricing. I personally recommend ASPHostPortal. ASPHostPortal has extremely affordable plans starting at $3.81 per month. The price covers everything from 5 GB of local storage for fast performance to a free SSL certification for customer trust to 24/7, 365 support to answer any questions you have about your website. Choosing the affordable web hosting by ASPHostPortal is a much better option for your business than risking everything for free hosting because you will always know what you are getting for the price.

Anjali Punjab

Anjali Punjab is a freelance writer, blogger, and ghostwriter who develops high-quality content for businesses. She is also a HubSpot Inbound Marketing Certified and Google Analytics Qualified Professional.