Host ASP.NET Core in a Windows Service

By Luke Latham and Tom Dykstra

An ASP.NET Core app can be hosted on Windows without using IIS as a Windows Service. When hosted as a Windows Service, the app can automatically start after reboots and crashes without requiring human intervention.

View or download sample code (how to download)

Get started

The following minimum changes are required to set up an existing ASP.NET Core project to run in a service:

  1. In the project file:

    1. Confirm the presence of the runtime identifier or add it to the <PropertyGroup> that contains the target framework:
      <PropertyGroup>
        <TargetFramework>netcoreapp2.1</TargetFramework>
        <RuntimeIdentifier>win7-x64</RuntimeIdentifier>
      </PropertyGroup>
      
    2. Add a package reference for Microsoft.AspNetCore.Hosting.WindowsServices.
  2. Make the following changes in Program.Main:

    • Call host.RunAsService instead of host.Run.

    • If the code calls UseContentRoot, use a path to the app's published location instead of Directory.GetCurrentDirectory().

      public static void Main(string[] args)
      {
          var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
          var pathToContentRoot = Path.GetDirectoryName(pathToExe);
      
          var host = WebHost.CreateDefaultBuilder(args)
              .UseContentRoot(pathToContentRoot)
              .UseStartup<Startup>()
              .Build();
      
          host.RunAsService();
      }
      
      public static void Main(string[] args)
      {
          var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
          var pathToContentRoot = Path.GetDirectoryName(pathToExe);
      
          var host = new WebHostBuilder()
              .UseKestrel()
              .UseContentRoot(pathToContentRoot)
              .UseIISIntegration()
              .UseStartup<Startup>()
              .Build();
      
          host.RunAsService();
      }
      
  3. Publish the app to a folder. Use dotnet publish or a Visual Studio publish profile that publishes to a folder.

    To publish the sample app from the command line, run the following command in a console window from the project folder:

    dotnet publish --configuration Release --output c:\svc
    
  4. Use the sc.exe command-line tool to create the service (sc create <SERVICE_NAME> binPath= "<PATH_TO_SERVICE_EXECUTABLE>"). The binPath value is the path to the app's executable, which includes the executable file name. The space between the equal sign and the quote character that starts the path is required.

    For the sample app and command that follows, the service is:

    • Named MyService.
    • Published to c:\svc folder.
    • Has an app executable named AspNetCoreService.exe.

    Open a command shell with administrative privileges and run the following command:

    sc create MyService binPath= "c:\svc\aspnetcoreservice.exe"
    

    Make sure the space is present between the binPath= argument and its value.

  5. Start the service with the sc start <SERVICE_NAME> command.

    To start the sample app service, use the following command:

    sc start MyService
    

    The command takes a few seconds to start the service.

  6. The sc query <SERVICE_NAME> command can be used to check the status of the service to determine its status:

    • START_PENDING
    • RUNNING
    • STOP_PENDING
    • STOPPED

    Use the following command to check the status of the sample app service:

    sc query MyService
    
  7. When the service is in the RUNNING state and if the service is a web app, browse the app at its path (by default, http://localhost:5000, which redirects to https://localhost:5001 when using HTTPS Redirection Middleware).

    For the sample app service, browse the app at http://localhost:5000.

  8. Stop the service with the sc stop <SERVICE_NAME> command.

    The following command stops the sample app service:

    sc stop MyService
    
  9. After a short delay to stop a service, uninstall the service with the sc delete <SERVICE_NAME> command.

    Check the status of the sample app service:

    sc query MyService
    

    When the sample app service is in the STOPPED state, use the following command to uninstall the sample app service:

    sc delete MyService
    

Provide a way to run outside of a service

It's easier to test and debug when running outside of a service, so it's customary to add code that calls RunAsService only under certain conditions. For example, the app can run as a console app with a --console command-line argument or if the debugger is attached:

public static void Main(string[] args)
{
    var isService = true;

    if (Debugger.IsAttached || args.Contains("--console"))
    {
        isService = false;
    }

    var pathToContentRoot = Directory.GetCurrentDirectory();

    if (isService)
    {
        var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
        pathToContentRoot = Path.GetDirectoryName(pathToExe);
    }

    var webHostArgs = args.Where(arg => arg != "--console").ToArray();

    var host = WebHost.CreateDefaultBuilder(webHostArgs)
        .UseContentRoot(pathToContentRoot)
        .UseStartup<Startup>()
        .Build();

    if (isService)
    {
        host.RunAsService();
    }
    else
    {
        host.Run();
    }
}

Because ASP.NET Core configuration requires name-value pairs for command-line arguments, the --console switch is removed before the arguments are passed to CreateDefaultBuilder.

public static void Main(string[] args)
{
    var isService = true;

    if (Debugger.IsAttached || args.Contains("--console"))
    {
        isService = false;
    }

    var pathToContentRoot = Directory.GetCurrentDirectory();

    if (isService)
    {
        var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
        pathToContentRoot = Path.GetDirectoryName(pathToExe);
    }

    var host = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(pathToContentRoot)
        .UseIISIntegration()
        .UseStartup<Startup>()
        .Build();

    if (isService)
    {
        host.RunAsService();
    }
    else
    {
        host.Run();
    }
}

Handle stopping and starting events

To handle OnStarting, OnStarted, and OnStopping events, make the following additional changes:

  1. Create a class that derives from WebHostService:

    internal class CustomWebHostService : WebHostService
    {
        public CustomWebHostService(IWebHost host) : base(host)
        {
        }
    
        protected override void OnStarting(string[] args)
        {
            base.OnStarting(args);
        }
    
        protected override void OnStarted()
        {
            base.OnStarted();
        }
    
        protected override void OnStopping()
        {
            base.OnStopping();
        }
    }
    
  2. Create an extension method for IWebHost that passes the custom WebHostService to ServiceBase.Run:

    public static class WebHostServiceExtensions
    {
        public static void RunAsCustomService(this IWebHost host)
        {
            var webHostService = new CustomWebHostService(host);
            ServiceBase.Run(webHostService);
        }
    }
    
  3. In Program.Main, call the new extension method, RunAsCustomService, instead of RunAsService:

    public static void Main(string[] args)
    {
        var isService = true;
    
        if (Debugger.IsAttached || args.Contains("--console"))
        {
            isService = false;
        }
    
        var pathToContentRoot = Directory.GetCurrentDirectory();
    
        if (isService)
        {
            var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
            pathToContentRoot = Path.GetDirectoryName(pathToExe);
        }
    
        var webHostArgs = args.Where(arg => arg != "--console").ToArray();
    
        var host = WebHost.CreateDefaultBuilder(args)
            .UseContentRoot(pathToContentRoot)
            .UseStartup<Startup>()
            .Build();
    
        if (isService)
        {
            host.RunAsCustomService();
        }
        else
        {
            host.Run();
        }
    }
    
    public static void Main(string[] args)
    {
        var isService = true;
    
        if (Debugger.IsAttached || args.Contains("--console"))
        {
            isService = false;
        }
    
        var pathToContentRoot = Directory.GetCurrentDirectory();
    
        if (isService)
        {
            var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
            pathToContentRoot = Path.GetDirectoryName(pathToExe);
        }
    
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(pathToContentRoot)
            .UseIISIntegration()
            .UseStartup<Startup>()
            .Build();
    
        if (isService)
        {
            host.RunAsCustomService();
        }
        else
        {
            host.Run();
        }
    }
    

If the custom WebHostService code requires a service from dependency injection (such as a logger), obtain it from the IWebHost.Services property:

internal class CustomWebHostService : WebHostService
{
    private ILogger _logger;

    public CustomWebHostService(IWebHost host) : base(host)
    {
        _logger = host.Services.GetRequiredService<ILogger<CustomWebHostService>>();
    }

    protected override void OnStarting(string[] args)
    {
        _logger.LogDebug("OnStarting method called.");
        base.OnStarting(args);
    }

    protected override void OnStarted()
    {
        _logger.LogDebug("OnStarted method called.");
        base.OnStarted();
    }

    protected override void OnStopping()
    {
        _logger.LogDebug("OnStopping method called.");
        base.OnStopping();
    }
}

Proxy server and load balancer scenarios

Services that interact with requests from the Internet or a corporate network and are behind a proxy or load balancer might require additional configuration. For more information, see Configure ASP.NET Core to work with proxy servers and load balancers.

Kestrel endpoint configuration

For information on Kestrel endpoint configuration, including HTTPS configuration and SNI support, see Kestrel endpoint configuration.