Modifying and extending Topshelf

Topshelf is a useful library if you have a .NET application that you would like to easily turn into a Windows Service.

A common approach is to create a console application, wire in Topshelf and then you can run it as either a console app or install and start it as a Windows service.

One thing I encountered this week was that by default Topshelf changes the current directory to the location of your .exe when your application runs. This makes sense because otherwise the default current directory for a Windows service is actually c:\windows\system32! If you’re trying to load additional files from the same directory as the .exe then that’s not where you’d want to be looking so Topshelf’s alteration makes sense.

But what about for debugging?

In Visual Studio, when you press F5 to debug a web project (which is the kind I was working with), the current directory defaults to the project’s directory. Again you may have logic that makes assumptions about other resources that need to be loaded relative to the project directory - if you’re now using Topshelf that’s going to break as the current directory will be changed. How to fix this?

Stepping through the Topshelf source code, I realised that I wanted a version of ConsoleRunHost that didn’t have the calls to Directory.SetCurrentDirectory(). That class is instantiated by the RunBuilder class, which in turn is returned by the DefaultHostBuilderFactory method in HostConfiguratorImpl.

So that’s the default factory, but the same class also provides a UseHostBuilder method that allows you to provide your own factory instead. That’s just what I needed.

By adding my own factory method that returned an instance of my own custom RunBuilder, which then returned a custom ConsoleRunHost implementation that was identical to Topshelf’s ConsoleRunHost with the exception that it didn’t change the default directory.

private static HostBuilder HostBuilderFactory(HostEnvironment environment, HostSettings settings)
{
    return new MyRunBuilder(environment, settings);
}

And tell Topshelf to use this method by calling UseHostBuilder:

var rc = HostFactory.Run(x =>
{
    x.UseHostBuilder(HostBuilderFactory);
    ...
}

If you wanted to, you could even check if you were running under the debugger to decide whether to use the default or custom behaviour.

It’s nice when libraries have thought about extensibility and provide hooks for consumers to swap in custom logic.

Written on August 17, 2019