共用方式為


使用 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 命令來建立新的 worker。 如果您使用 Visual Studio,範本會隱藏,直到安裝選擇性 ASP.NET 和 Web 開發工作負載為止。

先決條件

建立新專案

若要使用 Visual Studio 建立新的背景工作服務專案,請選取 [檔案]>[新增>專案...]。從 [[建立新專案] 對話框搜尋 [背景工作服務],然後選取 [背景工作服務] 範本。 如果您想要使用 .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,請執行下列命令。 (如果您使用 .NET 9 或更早版本的 SDK 版本,請改用 dotnet add package 表單。

dotnet package add Microsoft.Extensions.Hosting.WindowsServices

如需詳細資訊,請參閱 dotnet package add

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

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
  <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.6" />
</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="9.0.6" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.6" />
  </ItemGroup>
</Project>

上述項目檔變更會新增 <Nullable>enable<Nullable> 節點。 如需詳細資訊,請參閱 設定可空上下文

建立服務

將新的類別新增至名為 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一起插入。 這兩者都可以以欄位的形式提供給類別。 在 ExecuteAsync 方法中,笑話服務會要求一個笑話,並將它寫入記錄器。 在這裡情況下,記錄器是由 Windows 事件記錄檔實作 - Microsoft.Extensions.Logging.EventLog.EventLogLoggerProvider。 記錄檔會寫入,且可在 事件查看器中檢視。

注意

根據預設,事件記錄檔 嚴重性為 Warning。 這可以設定,但為了示範的目的,會使用 WindowsBackgroundService 擴充方法來記錄 LogWarning。 若要特別以 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 服務,建議您將應用程式發佈為單一檔案可執行檔。 由於沒有相依檔案散佈在系統中,因此擁有一個自包含的可執行檔較不容易出錯。 但是,您可以選擇不同的發佈方式,這是完全可接受的,只要您建立 *.exe 檔案,Windows 服務控制管理員可以設為目標。

重要

另一種發佈方法是建置 *.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="9.0.6" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.6" />
  </ItemGroup>
</Project>

項目檔的前幾行醒目提示會定義下列行為:

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

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

以滑鼠右鍵點擊 [方案總管]中的項目,然後選取 [發行]。 然後,選取 新增發佈配置檔 以建立配置檔。 從 [發佈] 對話框中,選取 [資料夾] 作為 [目標]。

Visual Studio 發佈對話框

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

Visual Studio 的設定檔配置

請確定已指定下列設定:

  • 部署模式:獨立式
  • 產生單一檔案: 已核取
  • 啟用 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) create 命令。 以系統管理員身分執行 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                 :

命令會輸出復原組態,這是預設值,因為它們尚未設定。

[Windows 服務復原組態屬性] 對話框。

若要設定復原,請使用 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.

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

已啟用重新啟動的 [Windows 服務復原組態屬性] 對話方塊。

服務復原選項和 .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 服務的應用程式,請開啟 Services。 選取 Windows 鍵(或 Ctrl + Esc),然後從 [服務] 搜尋。 從 Services 應用程式中,您應該能夠依其名稱尋找您的服務。

重要

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

服務使用者介面。

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

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

重要

若要偵錯應用程式,請確定您 嘗試偵錯在 Windows 服務程序中正在執行的可執行檔。

無法啟動程式。

啟動 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 記錄>應用程式 節點。 您應該會看到 警告 層級條目,且 Source 與應用程式的命名空間相匹配。 按兩下項目,或以滑鼠右鍵按下,然後選取 [事件屬性] 以查看詳細資料。

[事件屬性] 對話框,其中包含從服務記錄的詳細數據

事件記錄檔中看到記錄之後,您應該停止服務。 它的設計目的是每分鐘記錄一次隨機的笑話。 這是刻意的行為,但對於生產服務而言,缺乏 實用性。

停止 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 Service Control Manager 的 (sc.exe) delete 命令。 以系統管理員身分執行 PowerShell。

重要

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

sc.exe delete ".NET Joke Service"

您會看到輸出訊息:

[SC] DeleteService SUCCESS

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

另請參閱

下一步