Condividi tramite


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 di lavoro eseguiti come servizio Windows.

In questa esercitazione si apprenderà come:

  • Pubblicare un'app worker di .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 "Workers in .NET" è disponibile nel Samples Browser per il download. Per ulteriori informazioni, consulta Esempi di codice: Workers in .NET.

Importante

L'installazione di .NET SDK installa anche il Microsoft.NET.Sdk.Worker e il template 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 viene nascosto fino a quando non viene installato il carico di lavoro facoltativo ASP.NET e sviluppo Web.

Prerequisiti

Creare un nuovo progetto

Per creare un nuovo progetto del servizio di lavoro con Visual Studio, selezionare File>Nuovo progetto>. Nella finestra di dialogo Crea un nuovo progetto cercare "Servizio di lavoro" e selezionare Modello servizio 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 ulteriori informazioni sul comando del progetto new worker service di .NET CLI, vedere dotnet new worker.

Consiglio

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: Integrated Terminal.

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 aver aggiunto correttamente i pacchetti, il file di progetto dovrebbe ora contenere i riferimenti al pacchetto seguenti:

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

Aggiornare il file di progetto

Questo progetto di lavoro usa i tipi nullable di riferimento . 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="9.0.3" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.3" />
  </ItemGroup>
</Project>

Le modifiche apportate al file di progetto precedente 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 string che restituisce uno scherzo di programmazione casuale. Il campo _jokes a livello di classe viene utilizzato per archiviare l'elenco di barzellette. Uno scherzo casuale viene selezionato dall'elenco e restituito.

Riscrivere la classe Worker

Sostituire il 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, il JokeService viene inserito insieme a un ILogger. Entrambi vengono messi a disposizione della classe come dei campi. Nel metodo ExecuteAsync, il servizio di barzellette richiede una barzelletta e la scrive sul logger. In questo caso, il logger viene implementato dal registro eventi di Windows - Microsoft.Extensions.Logging.EventLog.EventLogLoggerProvider. I log vengono scritti in e sono disponibili per la visualizzazione nel Visualizzatore eventi .

Nota

Per impostazione predefinita, la gravità del registro eventi è Warning. Questa operazione può essere configurata, ma a scopo dimostrativo i log WindowsBackgroundService utilizzano il metodo di estensione LogWarning. Per specificare come destinazione il livello di EventLog, aggiungere una voce nelle appsettings. {Environment}.jsono 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 logging in .NET: Configurare Registro eventi di Windows.

Riscrivere la classe Program

Sostituire il modello Program.cs contenuto del file 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'iniezione delle dipendenze.

Per ulteriori informazioni sulla registrazione dei servizi, vedere iniezione delle dipendenze in .NET.

Pubblicare l'app

Per creare l'app del servizio di lavoro .NET come servizio Windows, è consigliabile pubblicare l'app come file eseguibile singolo. È meno incline agli errori avere un eseguibile indipendente, perché non ci sono file dipendenti sparsi nel file system. Tuttavia, è possibile scegliere una modalità di pubblicazione diversa, che è perfettamente accettabile, purché si crei un file *.exe che può essere destinato da Windows Service Control Manager.

Importante

Un approccio di pubblicazione alternativo consiste nel compilare il *.dll (invece di un *.exe) e quando si installa l'app pubblicata usando il Gestore Controllo Servizi di Windows si delega al .NET CLI e si passa la DLL. Per ulteriori informazioni, vedere .NET CLI: 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="9.0.3" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.3" />
  </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 di win-x64.
  • <PlatformTarget>x64</PlatformTarget>: Specificare 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 del 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 selezionare Pubblica. Selezionare quindi Aggiungi un profilo di pubblicazione per creare un profilo. Nella finestra di dialogo Pubblica selezionare cartella come destinazione.

la finestra di dialogo Pubblica di Visual Studio

Lascia posizione predefinitae quindi seleziona Fine. Dopo aver creato il profilo, selezionare Mostra tutte le impostazionie verificare le impostazioni del profilo .

Le impostazioni del profilo di Visual Studio

Verificare che siano specificate le impostazioni seguenti:

  • modalità di distribuzione: indipendente
  • Produrre un singolo file: selezionato
  • Abilitare la compilazione ReadyToRun: selezionata
  • Rimuovere gli assembly inutilizzati (in anteprima): non selezionato

Infine, selezionare Pubblica. L'app viene compilata e il file di .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 altre informazioni, vedere dotnet publish.

Importante

Con .NET 6, se si tenta di eseguire il debug dell'app con l'impostazione <PublishSingleFile>true</PublishSingleFile>, non sarà possibile eseguire il debug dell'app. Per altre informazioni, vedere Impossibile 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 create (sc.exe) nativo di Windows Service Control Manager. 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 il 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 creare.

Configurare il servizio Windows

Dopo aver creato il servizio, è possibile configurarlo facoltativamente. Se si è d'accordo con le impostazioni predefinite del servizio, 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 del 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.

finestra di dialogo Proprietà di configurazione del ripristino del servizio Windows.

Per configurare il ripristino, usare il sc.exe failure "<Service Name>" in cui <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.

finestra di dialogo Proprietà di configurazione del ripristino del servizio Windows con il riavvio abilitato.

Opzioni di ripristino del servizio e istanze di BackgroundService .NET

Con .NET 6, nuovi comportamenti di gestione delle eccezioni di hosting sono stati aggiunti a .NET. 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 Ignorare le eccezioni generate in BackgroundService.
StopHost Il IHost verrà arrestato quando viene generata un'eccezione non gestita.

Il comportamento predefinito prima di .NET 6 è Ignore, che ha generato processi zombie (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 si arresta in modo pulito, il che significa che 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. Considera il seguente blocco catch evidenziato.

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 Services. 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 di Windows. Per verificare che questa app funzioni come previsto, è necessario usare un account amministratore.

L'interfaccia utente di Services.

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

  • Avviare il servizio
  • Consultare i registri
  • Interrompere il servizio

Importante

Per eseguire il debug dell'applicazione, accertarsi di non tentare di eseguire il debug dell'eseguibile attivo all'interno del processo di Servizi Windows.

Impossibile avviare il programma.

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

Il stato del servizio passerà da START_PENDING a Attivo.

Visualizza 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. Dovresti vedere una voce di livello avviso con una sorgente che coincide con lo spazio dei nomi delle app. Fare doppio clic sulla voce oppure fare clic con il pulsante destro del mouse e selezionare Proprietà evento per visualizzare i dettagli.

finestra di dialogo Proprietà evento, con i dettagli registrati dal servizio

Dopo aver visualizzato i log nel registro eventi , dovresti arrestare il servizio. È progettato per registrare uno scherzo casuale una volta al minuto. Si tratta di un comportamento intenzionale, ma non pratico 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

Il servizio Status passerà da STOP_PENDING a Stopped.

Eliminare il servizio Windows

Per eliminare il servizio Windows, usare il comando di eliminazione (sc.exe) nativo di Windows Service Control Manager. Eseguire PowerShell come amministratore.

Importante

Se il servizio non è nello stato 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 eliminare.

Vedere anche

Prossimo