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>
      
      <PropertyGroup>
        <TargetFramework>netcoreapp2.0</TargetFramework>
        <RuntimeIdentifier>win7-x64</RuntimeIdentifier>
      </PropertyGroup>
      
      <PropertyGroup>
        <TargetFramework>netcoreapp1.1</TargetFramework>
        <RuntimeIdentifier>win7-x64</RuntimeIdentifier>
      </PropertyGroup>
      

      If mulitple Runtime Identifiers (RIDs) are provided in a semicolon-delimited list, use the property name <RuntimeIdentifiers> (plural). For more information, see .NET Core RID Catalog.

    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.

    • Call UseContentRoot and use a path to the app's published location instead of Directory.GetCurrentDirectory().

      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);
      
          return WebHost.CreateDefaultBuilder(args)
              .ConfigureAppConfiguration((context, config) =>
              {
                  // Configure the app here.
              })
              .UseContentRoot(pathToContentRoot)
              .UseStartup<Startup>();
      }
      
      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. Use dotnet publish or a Visual Studio publish profile. When using a Visual Studio, select the FolderProfile.

    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
    
  4. Use the sc.exe command-line tool to create the service. 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 at the start of the path is required.

    sc create <SERVICE_NAME> binPath= "<PATH_TO_SERVICE_EXECUTABLE>"
    

    For a service published in the project folder, use the path to the publish folder to create the service. In the following example:

    • The project resides in the c:\my_services\AspNetCoreService folder.
    • The project is published in Release configuration.
    • The Target Framework Moniker (TFM) is netcoreapp2.1.
    • The Runtime Identifer (RID) is win7-x64.
    • The app executable is named AspNetCoreService.exe.
    • The service is named MyService.

    Example:

    sc create MyService binPath= "c:\my_services\AspNetCoreService\bin\Release\netcoreapp2.1\win7-x64\publish\AspNetCoreService.exe"
    

    Important

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

    To publish and start the service from a different folder:

    1. Use the --output <OUTPUT_DIRECTORY> option on the dotnet publish command. If using Visual Studio, configure the Target Location in the FolderProfile publish property page before selecting the Publish button.
    2. Create the service with the sc.exe command using the output folder path. Include the name of the service's executable in the path provided to binPath.
  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 = !(Debugger.IsAttached || args.Contains("--console"));
    var builder = CreateWebHostBuilder(args.Where(arg => arg != "--console").ToArray());

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

    var host = builder.Build();

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

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            // Configure the app here.
        })
        .UseStartup<Startup>();

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.

Note

isService isn't passed from Main into CreateWebHostBuilder because the signature of CreateWebHostBuilder must be CreateWebHostBuilder(string[]) in order for integration testing to work properly.

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 = !(Debugger.IsAttached || args.Contains("--console"));
        var builder = CreateWebHostBuilder(args.Where(arg => arg != "--console").ToArray());
    
        if (isService)
        {
            var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
            var pathToContentRoot = Path.GetDirectoryName(pathToExe);
            builder.UseContentRoot(pathToContentRoot);
        }
    
        var host = builder.Build();
    
        if (isService)
        {
            host.RunAsCustomService();
        }
        else
        {
            host.Run();
        }
    }
    
    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                // Configure the app here.
            })
            .UseStartup<Startup>();
    

    Note

    isService isn't passed from Main into CreateWebHostBuilder because the signature of CreateWebHostBuilder must be CreateWebHostBuilder(string[]) in order for integration testing to work properly.

    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.

Configure HTTPS

Specify a Kestrel server HTTPS endpoint configuration.

Current directory and content root

The current working directory returned by calling Directory.GetCurrentDirectory() for a Windows Service is the C:\WINDOWS\system32 folder. The system32 folder isn't a suitable location to store a service's files (for example, settings files). Use one of the following approaches to maintain and access a service's assets and settings files with FileConfigurationExtensions.SetBasePath when using an IConfigurationBuilder:

  • Use the content root path. The IHostingEnvironment.ContentRootPath is the same path provided to the binPath argument when the service is created. Instead of using Directory.GetCurrentDirectory() to create paths to settings files, use the content root path and maintain the files in the app's content root.
  • Store the files in a suitable location on disk. Specify an absolute path with SetBasePath to the folder containing the files.

Additional resources