Creare un servizio Windows con BackgroundService

Gli sviluppatori di .NET Framework hanno probabilmente familiarità con le app del servizio Windows. Prima di .NET Core e .NET 5+, gli sviluppatori che si affidavano a .NET Framework potevano creare servizi Windows per eseguire attività in background o eseguire processi a esecuzione prolungata. Questa funzionalità è ancora disponibile ed è possibile creare servizi ruolo di lavoro eseguiti come servizio Windows.

Questa esercitazione illustra come:

  • Pubblicare un'app di ruolo di lavoro .NET come file eseguibile singolo.
  • Creare un servizio Windows.
  • Creare l'app BackgroundService come servizio Windows.
  • Avviare e arrestare il servizio Windows.
  • Visualizzare i registri eventi.
  • Eliminare il servizio Windows.

Suggerimento

Tutto il codice sorgente di esempio di "Servizi ruolo di lavoro in .NET" è disponibile per il download in Esplorazione esempi. Per altre informazioni, vedere Esplorare esempi di codice: Servizi ruolo di lavoro in .NET.

Importante

L'installazione di .NET SDK installa anche Microsoft.NET.Sdk.Worker e il modello di ruolo di lavoro. In altre parole, dopo aver installato .NET SDK, è possibile creare un nuovo ruolo di lavoro usando il comando dotnet new worker. Se si usa Visual Studio, il modello è nascosto fino a quando non viene installato il carico di lavoro facoltativo di ASP.NET e sviluppo Web.

Prerequisiti

Crea un nuovo progetto

Per creare un nuovo progetto del servizio ruolo di lavoro con Visual Studio, selezionare File>Nuovo>Progetto.... Nella finestra di dialogo Crea un nuovo progetto cercare "Servizio del ruolo di lavoro" e selezionare Modello del servizio ruolo di lavoro. Se si preferisce usare l'interfaccia della riga di comando di .NET, aprire il terminale preferito in una directory di lavoro. Eseguire il comando dotnet new e sostituire il <Project.Name> con il nome del progetto desiderato.

dotnet new worker --name <Project.Name>

Per altre informazioni sul comando new worker service project dell'interfaccia della riga di comando di .NET, vedere dotnet new worker.

Suggerimento

Se si usa Visual Studio Code, è possibile eseguire i comandi dell'interfaccia della riga di comando di .NET dal terminale integrato. Per altre informazioni, vedere Visual Studio Code: terminale integrato.

Installare il pacchetto NuGet

Per interagire con i servizi Windows nativi da implementazioni di IHostedService .NET, è necessario installare il pacchetto NuGet Microsoft.Extensions.Hosting.WindowsServices.

Per installarlo da Visual Studio, usare la finestra di dialogo Gestisci pacchetti NuGet.... Cercare "Microsoft.Extensions.Hosting.WindowsServices" e installarlo. Se si preferisce usare l'interfaccia della riga di comando di .NET, eseguire il comando dotnet add package:

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

Per altre informazioni sul comando add package dell'interfaccia della riga di comando di .NET, vedere dotnet add package.

Dopo l'aggiunta corretta dei pacchetti, il file di progetto dovrebbe ora contenere i riferimenti al pacchetto seguenti:

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

Aggiornare il file di progetto

Questo progetto di ruolo di lavoro usa tipi di riferimento che ammettono i valori Null di C#. Per abilitarli per l'intero progetto, aggiornare di conseguenza il file di progetto:

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

Le modifiche precedenti apportate al file di progetto aggiungono il nodo <Nullable>enable<Nullable>. Per altre informazioni, vedere Impostazione del contesto Nullable.

Creare il servizio

Aggiungere una nuova classe al progetto denominato JokeService.cse sostituirne il contenuto con il codice C# seguente:

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);

Il codice sorgente del servizio joke precedente espone una singola parte di funzionalità, il metodo GetJoke. Si tratta di un metodo che restituisce string che rappresenta una barzelletta casuale relativa alla programmazione. Il campo _jokes con ambito classe viene utilizzato per archiviare l'elenco di barzellette. Una barzelletta casuale viene selezionato dall'elenco e restituita.

Riscrivere la classe Worker

Sostituire la classe Worker esistente dal modello con il codice C# seguente e rinominare il file in 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);
        }
    }
}

Nel codice precedente JokeService viene inserito insieme a ILogger. Entrambi vengono resi disponibili per la classe come campi private readonly. Nel metodo ExecuteAsync il servizio joke richiede una barzelletta e la scrive nel logger. In questo caso il logger viene implementato dal Registro eventi di Windows - Microsoft.Extensions.Logging.EventLog.EventLogLogger. Vengono scritte informazioni nei log, che sono disponibili per la visualizzazione nel Visualizzatore eventi.

Nota

Per impostazione predefinita, la gravità del Registro eventi è Warning. Questa opzione può essere configurata, ma a scopo dimostrativo WindowsBackgroundService viene registrato con il metodo di estensione LogWarning. Per specificare come destinazione il livello EventLog, aggiungere una voce in appsettings.{Environment}.json o specificare un valore EventLogSettings.Filter.

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

Per altre informazioni sulla configurazione dei livelli di log, vedere Provider di registrazione in .NET: Configurare Windows EventLog.

Riscrivere la classe Program

Sostituire i contenuti del file Program.cs del modello con il codice C# seguente:

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();

Il metodo di estensione AddWindowsService configura l'app in modo che funzioni come servizio Windows. Il nome del servizio è impostato su ".NET Joke Service". Il servizio ospitato è registrato per l'inserimento delle dipendenze.

Per altre informazioni sulla registrazione dei servizi, vedere Inserimento delle dipendenze in .NET.

Pubblicazione dell'app

Per creare l'app del servizio ruolo di lavoro .NET come servizio Windows, è consigliabile pubblicare l'app come file eseguibile singolo. Un file eseguibile autonomo è meno soggetto a errori, perché non sono presenti file dipendenti nel file system. È tuttavia possibile scegliere una modalità di pubblicazione diversa, che è perfettamente accettabile, purché si crei un file *.exe che può essere usato da Gestione controllo servizi Windows.

Importante

Un approccio di pubblicazione alternativo consiste nel creare il file *.dll (invece di un file *.exe) e, quando si installa l'app pubblicata usando Gestione controllo dei servizi Windows, delegare all'interfaccia della riga di comando di .NET e passare la DLL. Per altre informazioni, vedere Interfaccia della riga di comando di .NET: comando 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>

Le righe evidenziate precedenti del file di progetto definiscono i comportamenti seguenti:

  • <OutputType>exe</OutputType>: crea un'applicazione console.
  • <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>: abilita la pubblicazione a file singolo.
  • <RuntimeIdentifier>win-x64</RuntimeIdentifier>: specifica il RID di win-x64.
  • <PlatformTarget>x64</PlatformTarget>: specifica la CPU della piattaforma di destinazione a 64 bit.

Per pubblicare l'app da Visual Studio, è possibile creare un profilo di pubblicazione persistente. Il profilo di pubblicazione è basato su XML e ha l'estensione file .pubxml. Visual Studio usa questo profilo per pubblicare l'app in modo implicito, mentre se si usa l'interfaccia della riga di comando di .NET, è necessario specificare in modo esplicito il profilo di pubblicazione per usarlo.

Fare clic con il pulsante destro del mouse sul progetto in Esplora soluzioni e scegliere Pubblica. Selezionare quindi Aggiungi un profilo di pubblicazione per creare un profilo. Nella finestra di dialogo Pubblica selezionare Cartella come Destinazione.

The Visual Studio Publish dialog

Lasciare il Percorso predefinitoe e quindi selezionare Fine. Dopo aver creato il profilo, selezionare Mostra tutte le impostazioni e verificare le Iimpostazioni del profilo.

The Visual Studio Profile settings

Verificare che siano specificate le impostazioni seguenti:

  • Modalità di distribuzione: autonoma
  • Produci un singolo file: opzione selezionata
  • Abilita la compilazione con ReadyToRun: opzione selezionata
  • Taglia gli assembly inutilizzati (in anteprima): opzione deselezionata

Selezionare infine Pubblica. L'app viene compilata e il file EXE risultante viene pubblicato nella directory di output /publish.

In alternativa, è possibile usare l'interfaccia della riga di comando di .NET per pubblicare l'app:

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

Per ulteriori informazioni, vedere dotnet publish.

Importante

Con .NET 6, se si prova a eseguire il debug dell'app con l'impostazione <PublishSingleFile>true</PublishSingleFile>, non sarà possibile eseguire il debug dell'app. Per altre informazioni, vedere Non è possibile connettersi a CoreCLR durante il debug di un'app .NET 6 "PublishSingleFile".

Creare il servizio Windows

Se non si ha familiarità con l'uso di PowerShell e si preferisce creare un programma di installazione per il servizio, vedere Creare un programma di installazione del servizio Windows. In caso contrario, per creare il servizio Windows usare il comando di creazione (sc.exe) nativo di Gestione controllo servizi Windows. Eseguire PowerShell come amministratore.

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

Suggerimento

Se è necessario modificare la radice del contenuto della configurazione host, è possibile passarla come argomento della riga di comando quando si specifica binpath:

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

Verrà visualizzato un messaggio di output:

[SC] CreateService SUCCESS

Per altre informazioni, vedere sc.exe create.

Configurare il servizio Windows

Dopo aver creato il servizio, è facoltativamente possibile configurarlo. Se le impostazioni predefinite del servizio sono appropriate per le esigenze specifiche, passare alla sezione Verificare la funzionalità del servizio.

I servizi Windows offrono opzioni di configurazione di ripristino. È possibile eseguire una query sulla configurazione corrente usando il comando sc.exe qfailure "<Service Name>" (dove <Service Name> è il nome dei servizi) per leggere i valori di configurazione di ripristino correnti:

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

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

Il comando restituirà la configurazione di ripristino, ovvero i valori predefiniti perché non sono ancora stati configurati.

The Windows Service recovery configuration properties dialog.

Per configurare il ripristino, usare sc.exe failure "<Service Name>" dove <Service Name> è il nome del servizio:

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

Suggerimento

Per configurare le opzioni di ripristino, la sessione del terminale deve essere eseguita come Amministratore.

Dopo la configurazione, è possibile eseguire di nuovo una query sui valori usando il comando 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.

Verranno visualizzati i valori di riavvio configurati.

The Windows Service recovery configuration properties dialog with restart enabled.

Opzioni di ripristino del servizio e istanze di BackgroundService .NET

Con .NET 6, sono stati aggiunti a .NET nuovi comportamenti di gestione delle eccezioni di hosting. L'enumerazione BackgroundServiceExceptionBehavior è stata aggiunta allo spazio dei nomi Microsoft.Extensions.Hosting e viene usata per specificare il comportamento del servizio quando viene generata un'eccezione. Nella tabella seguente sono elencate le opzioni disponibili:

Opzione Descrizione
Ignore Ignora le eccezioni generate in BackgroundService.
StopHost IHost verrà arrestato quando viene generata un'eccezione non gestita.

Il comportamento predefinito prima di .NET 6 è Ignore, che generava processi zombie, ovvero un processo in esecuzione che non ha eseguito alcuna operazione. Con .NET 6, il comportamento predefinito è StopHost, che comporta l'arresto dell'host quando viene generata un'eccezione. Ma l'arresto viene eseguito in modo pulito, ovvero il sistema di gestione dei servizi Windows non riavvia il servizio. Per consentire correttamente il riavvio del servizio, è possibile chiamare Environment.Exit con un codice di uscita diverso da zero. Si consideri il blocco catch evidenziato seguente:

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);
        }
    }
}

Verificare la funzionalità del servizio

Per visualizzare l'app creata come servizio Windows, aprire Servizi. Selezionare il tasto Windows (o CTRL + ESC) e cercare da "Servizi". Dall'app Servizi dovrebbe essere possibile trovare il servizio in base al nome.

Importante

Per impostazione predefinita, gli utenti normali (non amministratori) non possono gestire i servizi Windows. Per verificare che questa app funzioni come previsto, è necessario usare un account amministratore.

The Services user interface.

Per verificare che il servizio funzioni come previsto, è necessario:

  • avviare il servizio
  • Visualizzare i log
  • Interrompi il servizio

Importante

Per eseguire il debug dell'applicazione, assicurarsi di non provare a eseguire il debug dell'eseguibile in esecuzione attivamente all'interno del processo dei servizi Windows.

Unable to start program.

Avviare il servizio Windows

Per avviare il servizio Windows, usare il comando sc.exe start:

sc.exe start ".NET Joke Service"

Verrà visualizzato un output simile al seguente:

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

Lo Stato del servizio passerà da START_PENDING a In esecuzione.

Visualizzare i log

Per visualizzare i log, aprire il Visualizzatore eventi. Selezionare il tasto Windows (o CTRL + ESC) e cercare "Event Viewer". Selezionare il nodo Visualizzatore eventi (locale)>Registri di Windows>Applicazione. Verrà visualizzata una voce di livello Avviso con un'Origine corrispondente allo spazio dei nomi delle app. Fare doppio clic sulla voce oppure fare clic con il pulsante destro del mouse e scegliere Proprietà evento per visualizzare i dettagli.

The Event Properties dialog, with details logged from the service

Dopo aver visualizzato i log nel Registro eventi, è necessario arrestare il servizio. È progettato per registrare una barzelletta casuale una volta al minuto. Si tratta di un comportamento intenzionale, ma non risulta funzionale per i servizi di produzione.

Arrestare il servizio Windows

Per arrestare il servizio Windows, usare il comando sc.exe stop:

sc.exe stop ".NET Joke Service"

Verrà visualizzato un output simile al seguente:

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

Lo Stato del servizio passerà da STOP_PENDING ad Arrestato.

Eliminare il servizio Windows

Per eliminare il servizio Windows, usare il comando di eliminazione (sc.exe) nativo di Gestione controllo servizi Windows. Eseguire PowerShell come amministratore.

Importante

Se lo stato del servizio non è Arrestato, non verrà eliminato immediatamente. Assicurarsi che il servizio venga arrestato prima di eseguire il comando delete.

sc.exe delete ".NET Joke Service"

Verrà visualizzato un messaggio di output:

[SC] DeleteService SUCCESS

Per altre informazioni, vedere sc.exe delete.

Vedi anche

Avanti