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
- .NET 8.0 SDK o versioni successive
- Un sistema operativo Windows
- Ambiente di sviluppo integrato .NET (IDE)
- È possibile usare Visual Studio
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 diwin-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.
Lasciare il Percorso predefinitoe e quindi selezionare Fine. Dopo aver creato il profilo, selezionare Mostra tutte le impostazioni e verificare le Iimpostazioni del profilo.
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.
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.
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.
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.
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.
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
- Creare un programma di installazione del servizio Windows
- Servizi ruolo di lavoro in .NET
- Creare un servizio di accodamento
- Usare i servizi con ambito all'interno di una classe
BackgroundService
- Implementare l'interfaccia
IHostedService