I'm implementing a Web API using ASP.NET Core 2 (targeting .NET Framework 4.6.1) which is hosted in a Windows Service.
I've started with the example in Host an ASP.NET Core app in a Windows Service, but am having trouble implementing the following additional requirements:
When the Windows Service (or possibly the Web API it hosts) starts or restarts, I want to read some configuration (which happens to be in the Registry, but could be anywhere).
- If this configuration is missing or invalid then the Web API/Windows Service should not start (or should stop if has already started), should log some error message and, ideally, signal some failure to the Service Control Manager (e.g. ERROR_BAD_CONFIGURATION = 1610).
- If the configuration is present and valid, the Windows Service should start, start the Web API, and the configuration settings should be available to the Web API's Controllers (via Dependency Injection).
It isn't clear to me where in the Windows Service/Web application the logic to read & validate the custom config should go.
Depending on where I put the logic, if the config is invalid, there needs to be a graceful way to halt further start-up progress and to shutdown anything already started.
I can't see any obvious hooks for this in the ASP.NET, or Windows Services frameworks.
I've considered putting the read & validate logic in the following locations, but each comes with some problem(s):
- In Main(), before calling
IWebHost.RunAsService()
- In a notification method of
CustomWebHostService
(maybeOnStarting()
?) - During the initialisation of the Web API's
Startup
class (Configure()
orConfigureServices()
)
Can anyone shed any light onto how this can be done?
Option 1: In Main() before IWebHost.RunAsService()
This seems to be the cleanest way to fail fast, since if the config is missing/bad, then the Windows Service won't even be started.
However, this means that when the Service Control Manager starts the hosting executable, we won't register with it (via IWebHost.RunAsService()
) so the SCM returns the error:
[SC] StartService FAILED 1053:
The service did not respond to the start or control request in a timely fashion.
Ideally, the Service Control Manager should be aware of the reason for failure to start-up, and could then log this to the Event Log.
I don't think this is possible unless we register with the Service Control Manager, which brings us to Option 2.
Option 2: During the Windows Service startup
In a previous incarnation of the Web API (in WCF) I subclassed ServiceBase
, acquired and validated config in ServiceBaseSubclass.OnStart()
and, if the config was invalid, set this.ExitCode
and called Stop()
as per the suggestion in What is the proper way for a Windows service to fail?. Like this:
partial class WebServicesHost : ServiceBase
{
private ServiceHost _webServicesServiceHost;
// From https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx
private const int ErrorBadConfiguration = 1610;
protected override void OnStart(string[] args)
{
base.OnStart(args);
var customConfig = ReadAndValidateCustomConfig();
if (customConfig != null)
{
var webService = new WebService(customConfig);
_webServicesServiceHost = new ServiceHost(webService);
_webServicesServiceHost.Open();
}
else
{
// Configuration is bad, stop the service
ExitCode = ErrorBadConfiguration;
Stop();
}
}
}
When you then used the Service Control Manager to start and query the Windows Service, it correctly reported the failure:
C:\> sc start MyService
SERVICE_NAME: MyService
TYPE : 10 WIN32_OWN_PROCESS
STATE : 2 START_PENDING
(NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x7d0
PID : 10764
FLAGS :
C:\> sc query MyService
SERVICE_NAME: MyService
TYPE : 10 WIN32_OWN_PROCESS
STATE : 1 STOPPED <-- Service is stopped
WIN32_EXIT_CODE : 1610 (0x64a) <-- and reports the reason for stopping
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
In ASP.NET Core 2, if I subclass WebHostService
(as described in Host an ASP.NET Core app in a Windows Service), then there only appear to be hooks for notification of the Windows Service's startup workflow progress (e.g. OnStarting()
or OnStarted()
), but no mention of how to stop the service safely.
Looking at the WebHostService
source and decompiling ServiceBase
makes me think that it would be a very bad idea to call ServiceBase.Stop()
from CustomWebHostService.OnStarting()
since I think this leads to using a disposed object.
class CustomWebHostService : WebHostService
{
// From https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx
private const int ErrorBadConfiguration = 1610;
public CustomWebHostService(IWebHost host) : base(host)
{
}
protected override void OnStarting(string[] args)
{
base.OnStarting(args);
var customConfig = ReadAndValidateCustomConfig();
if (customConfig == null)
{
ExitCode = ErrorBadConfiguration;
Stop();
}
}
}
// Code from https://github.com/aspnet/Hosting/blob/2.0.1/src/Microsoft.AspNetCore.Hosting.WindowsServices/WebHostService.cs
public class WebHostService : ServiceBase
{
protected sealed override void OnStart(string[] args)
{
OnStarting(args);
_host
.Services
.GetRequiredService<IApplicationLifetime>()
.ApplicationStopped
.Register(() =>
{
if (!_stopRequestedByWindows)
{
Stop();
}
});
_host.Start();
OnStarted();
}
protected sealed override void OnStop()
{
_stopRequestedByWindows = true;
OnStopping();
_host?.Dispose();
OnStopped();
}
Specifically, CustomWebHostService.OnStarting()
would call ServiceBase.Stop()
which, in turn, would call WebHostService.OnStop()
which disposes this._host
. Then the second statement in WebServiceHost.OnStart()
is executed, using this._host
after it has been disposed.
Confusingly, this approach actually appears to "work" since CustomWebHostService.OnStarted()
doesn't end up getting called in my experiments. However, I suspect this is because an exception is thrown beforehand. This doesn't seem like something that should be relied upon, so doesn't feel like a particularly robust solution.
What is the proper way for a Windows service to fail during its startup suggests a different way, by deferring the ServiceBase.Stop()
call (i.e. letting the Windows Service start, then stopping it the config turned out to be bad) but this appears to allow the Web API to begin starting-up before sweeping the legs out from under it by stopping the Windows Service at some arbitrary time in the future.
I think I'd rather not start the Web API if the Windows Service can tell that it shouldn't start, or get the Web API start-up to read the config and shut itself down (see Option 3).
Also, it is still not clear how the customConfig
instance could be made available to the ASP.NET Core 2 Web Service classes.
Option 3: During the initialisation of the Web Service's Startup class
This would seem to be the best location for reading the configuration since the config logically belongs to the Web Service, and is not related to hosting.
I discovered the IApplicationLifetime
interface has a StopApplication()
method (see IApplicationLifetime section of Hosting in ASP.NET Core). This seems ideal.
public class Program
{
public static void Main(string[] args)
{
if (Debugger.IsAttached)
{
BuildWebHost(args).Run();
}
else
{
BuildWebHost(args).RunAsCustomService();
}
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseUrls("http://*:5000")
.Build();
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IApplicationLifetime appLifetime, IHostingEnvironment env)
{
appLifetime.ApplicationStarted.Register(OnStarted);
appLifetime.ApplicationStopping.Register(OnStopping);
appLifetime.ApplicationStopped.Register(OnStopped);
var customConfig = ReadAndValidateCustomConfig();
if (customConfig != null)
{
// TODO: Somehow make config available to MVC controllers
app.UseMvc();
}
else
{
appLifetime.StopApplication();
}
}
private void OnStarted()
{
// Perform post-startup activities here
Console.WriteLine("OnStarted");
}
private void OnStopping()
{
// Perform on-stopping activities here
Console.WriteLine("OnStopping");
}
private void OnStopped()
{
// Perform post-stopped activities here
Console.WriteLine("OnStopped");
}
}
This "works" when debugging in Visual Studio (i.e. the process starts stand-alone and serves the Web API if the config is good, or shuts down cleanly if the config is bad), although the log messages appear in a slightly odd order which makes me suspicious:
OnStopping
OnStarted
Hosting environment: Development
Content root path: [redacted]
Now listening on: http://[::]:5000
Application started. Press Ctrl+C to shut down.
OnStopped
When starting as a Windows Service where the config is invalid, the Service remains running, but the Web API always responds with 404 (Not Found).
Presumably, this means that the Web API has shut down, but the Windows Service hasn't been able to notice this, so hasn't shut itself down.
Looking again at the IApplication documentation, I notice it says:
The IApplicationLifetime interface allows you to perform post-startup and shutdown activities.
This suggests that I really shouldn't be calling StopApplication()
during the start-up sequence. However, I can't see a way of deferring the call until after the application has started.
If that is not possible, is there another way to signal to ASP.NET Core 2 that the Startup.Configure()
function has failed and the application should shutdown?
Conclusion
Any suggestions on a good way to achieve the above requirements would be welcomed, as well as pointing out that I've grossly misunderstood Windows Services and/or ASP.NET Core 2! :-)