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 automatically starts after reboots.

View or download sample code (how to download)

Convert a project into a Windows Service

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

  1. In the project file:

    • Confirm the presence of a Windows Runtime Identifier (RID) or add it to the <PropertyGroup> that contains the target framework:

      <PropertyGroup>
         <TargetFramework>netcoreapp2.2</TargetFramework>
         <RuntimeIdentifier>win7-x64</RuntimeIdentifier>
      </PropertyGroup>
      

      To publish for multiple RIDs:

      • Provide the RIDs in a semicolon-delimited list.
      • Use the property name <RuntimeIdentifiers> (plural).

      For more information, see .NET Core RID Catalog.

    • 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>();
      }
      
  3. Publish the app using dotnet publish, a Visual Studio publish profile, or Visual Studio Code. When using Visual Studio, select the FolderProfile and configure the Target Location before selecting the Publish button.

    To publish the sample app using command-line interface (CLI) tools, run the dotnet publish command at a command prompt from the project folder. The RID must be specified in the <RuntimeIdenfifier> (or <RuntimeIdentifiers>) property of the project file. In the following example, the app is published in Release configuration for the win7-x64 runtime to a folder created at c:\svc:

    dotnet publish --configuration Release --runtime win7-x64 --output c:\svc
    
  4. Create a user account for the service using the net user command:

    net user {USER ACCOUNT} {PASSWORD} /add
    

    For the sample app, create a user account with the name ServiceUser and a password. In the following command, replace {PASSWORD} with a strong password.

    net user ServiceUser {PASSWORD} /add
    

    If you need to add the user to a group, use the net localgroup command, where {GROUP} is the name of the group:

    net localgroup {GROUP} {USER ACCOUNT} /add
    

    For more information, see Service User Accounts.

  5. Grant write/read/execute access to the app's folder using the icacls command:

    icacls "{PATH}" /grant {USER ACCOUNT}:(OI)(CI){PERMISSION FLAGS} /t
    
    • {PATH} – Path to the app's folder.
    • {USER ACCOUNT} – The user account (SID).
    • (OI) – The Object Inherit flag propagates permissions to subordinate files.
    • (CI) – The Container Inherit flag propagates permissions to subordinate folders.
    • {PERMISSION FLAGS} – Sets the app's access permissions.
      • Write (W)
      • Read (R)
      • Execute (X)
      • Full (F)
      • Modify (M)
    • /t – Apply recursively to existing subordinate folders and files.

    For the sample app published to the c:\svc folder and the ServiceUser account with write/read/execute permissions, use the following command:

    icacls "c:\svc" /grant ServiceUser:(OI)(CI)WRX /t
    

    For more information, see icacls.

  6. 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 of each parameter and value is required.

    sc create {SERVICE NAME} binPath= "{PATH}" obj= "{DOMAIN}\{USER ACCOUNT}" password= "{PASSWORD}"
    
    • {SERVICE NAME} – The name to assign to the service in Service Control Manager.
    • {PATH} – The path to the service executable.
    • {DOMAIN} (or if the machine isn't domain joined, the local machine name) and {USER ACCOUNT} – The domain (or local machine name) and user account under which the service runs. Do not omit the obj parameter. The default value for obj is the LocalSystem account account. Running a service under the LocalSystem account presents a significant security risk. Always run a service under a user account with restricted privileges on the server.
    • {PASSWORD} – The user account password.

    In the following example:

    • The service is named MyService.
    • The published service resides in the c:\svc folder. The app executable is named AspNetCoreService.exe. The binPath value is enclosed in straight quotation marks (").
    • The service runs under the ServiceUser account. Replace {DOMAIN} with the user account's domain or local machine name. Enclose the obj value in straight quotation marks ("). Example: If the hosting system is a local machine named MairaPC, set obj to "MairaPC\ServiceUser".
    • Replace {PASSWORD} with the user account's password. The password value is enclosed in straight quotation marks (").
    sc create MyService binPath= "c:\svc\aspnetcoreservice.exe" obj= "{DOMAIN}\ServiceUser" password= "{PASSWORD}"
    

    Important

    Make sure that the spaces between the parameters' equal signs and the parameters' values are present.

  7. 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.

  8. To check the status of the service, use the sc query {SERVICE NAME} command. The status is reported as one of the following values:

    • START_PENDING
    • RUNNING
    • STOP_PENDING
    • STOPPED

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

    sc query MyService
    
  9. 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.

  10. Stop the service with the sc stop {SERVICE NAME} command.

    The following command stops the sample app service:

    sc stop MyService
    
  11. 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
    

Run the app 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.

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.

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

To configure the service with a secure endpoint:

  1. Create an X.509 certificate for the hosting system using your platform's certificate acquisition and deployment mechanisms.
  2. Specify a Kestrel server HTTPS endpoint configuration to use the certificate.

Use of the ASP.NET Core HTTPS development certificate to secure a service endpoint isn't supported.

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