使用 BackgroundService 建立 Windows 服務

.NET Framework 開發人員可能已熟悉 Windows 服務應用程式。 在 .NET Core 和 .NET 5+ 之前,依賴 .NET Framework 的開發人員可能會建立 Windows 服務,以執行背景工作或執行長時間的流程。 此功能仍可供使用,而且您可以建立以 Windows 服務執行的背景工作服務。

在本教學課程中,您將了解如何:

  • 將 .NET 背景工作角色應用程式發佈為單一檔案可執行檔。
  • 建立 Windows 服務。
  • 建立 BackgroundService 應用程式作為 Windows 服務。
  • 啟動和停止 Windows 服務。
  • 檢視事件記錄檔。
  • 刪除 Windows 服務。

提示

範例瀏覽器中提供所有「.NET 中的背景工作角色」範例原始程式碼以供下載。 如需詳細資訊,請參閱瀏覽程式碼範例:.NET 中的背景工作角色

重要

安裝 .NET SDK,也會安裝 Microsoft.NET.Sdk.Worker 和背景工作角色範本。 換句話說,安裝 .NET SDK 之後,您可以使用 dotnet new worker 命令建立新的背景工作角色。 如果您使用 Visual Studio,在安裝選擇性的 ASP.NET 和 Web 開發工作負載前,範本將會隱藏。

必要條件

建立新專案

若要使用 Visual Studio Code 建立新的背景工作服務專案,您將選取 [檔案]>[新增]>[專案...]。從 [建立新專案] 對話方塊搜尋 [背景工作服務],然後選取 [背景工作服務] 範本。 如果您想要使用 .NET CLI,請在工作目錄中開啟您慣用的終端。 執行 dotnet new 命令,並以您想要的專案名稱取代 <Project.Name>

dotnet new worker --name <Project.Name>

如需 .NET CLI 新的背景工作角色服務專案命令的詳細資訊,請參閱 dotnet new worker

提示

如果您使用 Visual Studio Code,您可以從整合式終端執行 .NET CLI 命令。 如需詳細資訊,請參閱 Visual Studio Code:整合式終端

安裝 NuGet 套件

若要從 .NET IHostedService 實作與原生 Windows 服務互通,您必須安裝 Microsoft.Extensions.Hosting.WindowsServices NuGet 套件

若要從 Visual Studio 安裝此專案,請使用 [管理 NuGet 套件...] 對話方塊。 搜尋 「Microsoft.Extensions.Hosting.WindowsServices」,並加以安裝。 如果您想要使用 .NET CLI,請執行 dotnet add package 命令:

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

如需 .NET CLI 新增套件命令的詳細資訊,請參閱 dotnet add package

成功新增套件之後,您的專案檔現在應該包含下列套件參考:

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
</ItemGroup>

更新專案檔

此背景工作專案會使用 C# 的可為 Null 的參考型別。 若要在整個專案中啟用,請依此更新專案檔:

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
  </ItemGroup>
</Project>

上述專案檔變更會新增 <Nullable>enable<Nullable> 節點。 如需詳細資訊,請參閱設定可為 Null 的內容

建立服務

將新的類別新增至名為 JokeService.cs 的專案,並用下列 C# 程式碼取代其內容:

namespace App.WindowsService;

public sealed class JokeService
{
    public string GetJoke()
    {
        Joke joke = _jokes.ElementAt(
            Random.Shared.Next(_jokes.Count));

        return $"{joke.Setup}{Environment.NewLine}{joke.Punchline}";
    }

    // Programming jokes borrowed from:
    // https://github.com/eklavyadev/karljoke/blob/main/source/jokes.json
    private readonly HashSet<Joke> _jokes = new()
    {
        new Joke("What's the best thing about a Boolean?", "Even if you're wrong, you're only off by a bit."),
        new Joke("What's the object-oriented way to become wealthy?", "Inheritance"),
        new Joke("Why did the programmer quit their job?", "Because they didn't get arrays."),
        new Joke("Why do programmers always mix up Halloween and Christmas?", "Because Oct 31 == Dec 25"),
        new Joke("How many programmers does it take to change a lightbulb?", "None that's a hardware problem"),
        new Joke("If you put a million monkeys at a million keyboards, one of them will eventually write a Java program", "the rest of them will write Perl"),
        new Joke("['hip', 'hip']", "(hip hip array)"),
        new Joke("To understand what recursion is...", "You must first understand what recursion is"),
        new Joke("There are 10 types of people in this world...", "Those who understand binary and those who don't"),
        new Joke("Which song would an exception sing?", "Can't catch me - Avicii"),
        new Joke("Why do Java programmers wear glasses?", "Because they don't C#"),
        new Joke("How do you check if a webpage is HTML5?", "Try it out on Internet Explorer"),
        new Joke("A user interface is like a joke.", "If you have to explain it then it is not that good."),
        new Joke("I was gonna tell you a joke about UDP...", "...but you might not get it."),
        new Joke("The punchline often arrives before the set-up.", "Do you know the problem with UDP jokes?"),
        new Joke("Why do C# and Java developers keep breaking their keyboards?", "Because they use a strongly typed language."),
        new Joke("Knock-knock.", "A race condition. Who is there?"),
        new Joke("What's the best part about TCP jokes?", "I get to keep telling them until you get them."),
        new Joke("A programmer puts two glasses on their bedside table before going to sleep.", "A full one, in case they gets thirsty, and an empty one, in case they don’t."),
        new Joke("There are 10 kinds of people in this world.", "Those who understand binary, those who don't, and those who weren't expecting a base 3 joke."),
        new Joke("What did the router say to the doctor?", "It hurts when IP."),
        new Joke("An IPv6 packet is walking out of the house.", "He goes nowhere."),
        new Joke("3 SQL statements walk into a NoSQL bar. Soon, they walk out", "They couldn't find a table.")
    };
}

readonly record struct Joke(string Setup, string Punchline);

上述的惡作劇服務原始程式碼會公開單一部份功能,也就是 GetJoke 方法。 這是一種 string 傳回方法,代表隨機程式設計惡作劇。 類別範圍 _jokes 欄位是用來儲存惡作劇清單。 系統會從清單中選取隨機惡作劇並傳回。

重寫 Worker 類別

以下列 C# 程式碼取代範本的現有 Worker,並將檔案重新命名為 WindowsBackgroundService.cs

namespace App.WindowsService;

public sealed class WindowsBackgroundService(
    JokeService jokeService,
    ILogger<WindowsBackgroundService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = jokeService.GetJoke();
                logger.LogWarning("{Joke}", joke);

                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // When the stopping token is canceled, for example, a call made from services.msc,
            // we shouldn't exit with a non-zero exit code. In other words, this is expected...
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "{Message}", ex.Message);

            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }
}

在上述程式碼中,JokeService 會與 ILogger 一起插入。 這兩者都可當做 private readonly 欄位供類別使用。 在 ExecuteAsync 方法中,惡作劇服務會要求惡作劇,並將其寫入記錄器。 在此情況下,記錄器是由 Windows 事件記錄檔進行實作 - Microsoft.Extensions.Logging.EventLog.EventLogLogger。 記錄會寫入 [事件檢視器],且可在其中檢視。

注意

根據預設,「事件記錄檔」嚴重性為 Warning。 這可加以設定,但基於示範目的,請使用 LogWarning 擴充方法記錄 WindowsBackgroundService。 若要特別以 EventLog 層級為目標,請在 appsettings {Environment}.json 中新增輸入,或提供 EventLogSettings.Filter 值。

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    },
    "EventLog": {
      "SourceName": "The Joke Service",
      "LogName": "Application",
      "LogLevel": {
        "Microsoft": "Information",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
}

如需設定記錄層級的詳細資訊,請參閱 .NET 中的記錄提供者:設定 Windows EventLog

重寫 Program 類別

使用下列 C# 程式碼取代範本 Program.cs 檔案內容:

using App.WindowsService;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = ".NET Joke Service";
});

LoggerProviderOptions.RegisterProviderOptions<
    EventLogSettings, EventLogLoggerProvider>(builder.Services);

builder.Services.AddSingleton<JokeService>();
builder.Services.AddHostedService<WindowsBackgroundService>();

IHost host = builder.Build();
host.Run();

AddWindowsService 擴充方法會設定應用程式如 Windows 服務般運作。 服務名稱已設為 ".NET Joke Service"。 託管服務已完成相依性插入註冊。

如需註冊服務的詳細資訊,請參閱 .NET 中的相依性插入

發行應用程式

若要將 .NET 背景工作服務應用程式建立為 Windows 服務,建議您將應用程式發佈為單一檔案可執行檔。 由於檔案系統上沒有任何相依的檔案,因此較不容易有獨立式的可執行檔。 然而,您可以選擇可完全接受的不同發佈形式,只需建立可由 Windows 服務控制管理員鎖定目標的 *.exe 檔案即可。

重要

替代的發佈方法是建置 *.dll (而非 *.exe),並在您使用 Windows 服務控制管理員安裝已發佈的應用程式時,委派給 .NET CLI 並傳遞 DLL。 如需詳細資訊,請參閱 .NET CLI:dotnet 命令

sc.exe create ".NET Joke Service" binpath="C:\Path\To\dotnet.exe C:\Path\To\App.WindowsService.dll"
<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
    <OutputType>exe</OutputType>
    <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PlatformTarget>x64</PlatformTarget>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
  </ItemGroup>
</Project>

上述專案檔的醒目提示行會定義下列行為:

  • <OutputType>exe</OutputType>:建立主控台應用程式 (Console Application)。
  • <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>:啟用單一檔案發佈。
  • <RuntimeIdentifier>win-x64</RuntimeIdentifier>:指定 win-x64RID
  • <PlatformTarget>x64</PlatformTarget>:指定 64 位元的目標平台 CPU。

若要從 Visual Studio 發佈應用程式,您可以建立已保存發行設定檔。 發行設定檔以 XML 為基礎,且副檔名為 .pubxml。 Visual Studio 會隱含地使用此設定檔來發佈應用程式,而如果您使用 .NET CLI,則必須明確指定要使用的發行設定檔。

以滑鼠右鍵按一下 [方案總管] 中的專案,然後選取 [發佈]。 然後,選取 [新增發行設定檔] 以建立設定檔。 從 [發佈] 對話方塊中,選取 [資料夾] 作為您的 [目標]

The Visual Studio Publish dialog

保留預設 [位置],然後選取 [完成]。 建立設定檔之後,請選取 [顯示所有設定],然後確認您的 [設定檔設定]

The Visual Studio Profile settings

請確定已指定下列設定:

  • 部署模式:獨立式
  • 產生單一檔案:已選取
  • 啟用 ReadyToRun 編譯:已選取
  • 修剪未使用的組件 (預覽):未選取

最後,選取 [發佈]。 應用程式會進行編譯,而產生的.exe 檔案會發佈至 /publish 輸出目錄。

或者,您可以使用 .NET CLI 來發佈應用程式:

dotnet publish --output "C:\custom\publish\directory"

如需詳細資訊,請參閱dotnet publish

重要

使用 .NET 6 時,如果您嘗試使用 <PublishSingleFile>true</PublishSingleFile> 設定對應用程式進行偵錯,您將無法對應用程式進行偵錯。 如需詳細資訊,請參閱進行 'PublishSingleFile' .NET 6 應用程式偵錯時無法附加至 CoreCLR

建立 Windows 服務

如果您不熟悉使用 PowerShell,而您想要為服務建立安裝程式,請參閱 建立 Windows 服務安裝程式。 或者,若要建立 Windows 服務,請使用原生 Windows 服務控制管理員的 (sc.exe) 來建立命令。 以系統管理員身分執行 PowerShell。

sc.exe create ".NET Joke Service" binpath="C:\Path\To\App.WindowsService.exe"

提示

如果您需要變更主機設定的內容根目錄,您可以在指定 binpath 時將其作為命令列引數傳遞:

sc.exe create "Svc Name" binpath="C:\Path\To\App.exe --contentRoot C:\Other\Path"

您會看到輸出訊息:

[SC] CreateService SUCCESS

如需詳細資訊,請參閱建立 sc.exe

設定 Windows 服務

建立服務之後,您可以選擇性進行設定。 如果您使用的是服務預設值,請跳至驗證服務功能區段。

Windows 服務提供復原設定選項。 您可以使用 sc.exe qfailure "<Service Name>" (其中 <Service Name> 是您服務的名稱) 命令查詢目前的設定,讀取目前的復原設定值:

sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: .NET Joke Service
        RESET_PERIOD (in seconds)    : 0
        REBOOT_MESSAGE               :
        COMMAND_LINE                 :

此命令會輸出復原設定,這是預設值,因為尚未進行設定。

The Windows Service recovery configuration properties dialog.

若要設定復原,請使用 sc.exe failure "<Service Name>",其中 <Service Name> 是您服務的名稱:

sc.exe failure ".NET Joke Service" reset=0 actions=restart/60000/restart/60000/run/1000
[SC] ChangeServiceConfig2 SUCCESS

提示

若要設定復原選項,您的終端機工作階段必須以系統管理員身分執行。

成功設定之後,您可以使用 sc.exe qfailure "<Service Name>" 命令再次查詢值:

sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: .NET Joke Service
        RESET_PERIOD (in seconds)    : 0
        REBOOT_MESSAGE               :
        COMMAND_LINE                 :
        FAILURE_ACTIONS              : RESTART -- Delay = 60000 milliseconds.
                                       RESTART -- Delay = 60000 milliseconds.
                                       RUN PROCESS -- Delay = 1000 milliseconds.

您會看到已設定的重新啟動值。

The Windows Service recovery configuration properties dialog with restart enabled.

服務復原選項和 .NET BackgroundService 執行個體

.NET 6 在 .NET 中新增了新的裝載例外狀況處理行為BackgroundServiceExceptionBehavior 列舉已加入 Microsoft.Extensions.Hosting 命名空間,並用來指定擲回例外狀況時的服務行為。 下表列出可用選項:

選項 描述
Ignore 忽略 BackgroundService 中擲回的例外狀況。
StopHost 擲回未處理的例外狀況時,將會停止 IHost

.NET 6 之前的預設行為是 Ignore,這會導致「廢止流程」(未執行任何動作的執行流程)。 .NET 6 的預設行為是 StopHost,這會導致主機在例外狀況擲回時停止。 然而,主機會完全停止,意即 Windows 服務管理系統不會重新啟動服務。 若要正確允許服務重新啟動,您可以使用非零結束代碼呼叫 Environment.Exit。 請考慮下列醒目提示的 catch 區塊:

namespace App.WindowsService;

public sealed class WindowsBackgroundService(
    JokeService jokeService,
    ILogger<WindowsBackgroundService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = jokeService.GetJoke();
                logger.LogWarning("{Joke}", joke);

                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // When the stopping token is canceled, for example, a call made from services.msc,
            // we shouldn't exit with a non-zero exit code. In other words, this is expected...
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "{Message}", ex.Message);

            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }
}

驗證服務功能

若要查看建立為 Windows 服務的應用程式,請開啟 [服務]。 選取 Windows 鍵 (或 Ctrl + Esc),然後從 [服務] 搜尋。 從 [服務] 應用程式,您應能夠依其名稱尋找您的服務。

重要

根據預設,一般 (非管理員) 使用者無法管理 Windows 服務。 若要確認此應用程式如預期運作,您必須使用管理帳戶。

The Services user interface.

若要確認服務依預期運作,您需要:

  • 啟動服務
  • 檢視記錄檔
  • 停止此服務

重要

若要偵錯應用程式,請確定您「未」嘗試對正在 Windows 服務流程內執行的可執行檔進行偵錯。

Unable to start program.

啟動 Windows 服務

若要啟動 Windows 服務,請使用 sc.exe start 命令:

sc.exe start ".NET Joke Service"

您會看到類似下方的輸出:

SERVICE_NAME: .NET Joke Service
    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                : 37636
    FLAGS

服務 [狀態] 將會從 START_PENDING 轉換為 [執行中]

檢視記錄

若要檢視記錄,請開啟 [事件檢視器]。 選取 Windows 鍵 (或 Ctrl + Esc),然後搜尋 "Event Viewer"。 選取 [事件檢視器 (本機)]> [Windows 記錄]> [應用程式] 節點。 您應可看到 [警告] 層級項目,其中包含符合應用程式命名空間的 [來源]。 按兩下專案,或以滑鼠右鍵按一下並選取 [事件屬性] 以檢視詳細資料。

The Event Properties dialog, with details logged from the service

在 [事件記錄檔] 中看到記錄之後,您應停止服務。 其設計目的在於將隨機惡作劇每分鐘記錄一次。 這是刻意的行為,但「不」適用於生產服務。

停止 Windows 服務

若要停止 Windows 服務,請使用 sc.exe stop 命令:

sc.exe stop ".NET Joke Service"

您會看到類似下方的輸出:

SERVICE_NAME: .NET Joke Service
    TYPE               : 10  WIN32_OWN_PROCESS
    STATE              : 3  STOP_PENDING
                            (STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)
    WIN32_EXIT_CODE    : 0  (0x0)
    SERVICE_EXIT_CODE  : 0  (0x0)
    CHECKPOINT         : 0x0
    WAIT_HINT          : 0x0

服務 [狀態] 將會從 STOP_PENDING 轉換為 [已停止]

刪除 Windows 服務

若要刪除 Windows 服務,請使用原生 Windows 服務控制管理員的 (sc.exe) delete 命令。 以系統管理員身分執行 PowerShell。

重要

如果服務不是處於 [已停止] 狀態,將不會立即刪除。 在發出 delete 命令之前,請確定服務已停止運作。

sc.exe delete ".NET Joke Service"

您會看到輸出訊息:

[SC] DeleteService SUCCESS

如需詳細資訊,請參閱sc.exe delete

另請參閱

下一步