Créer un service Windows avec BackgroundService

Les développeurs .NET Framework sont probablement familiarisés avec les applications de service Windows. Avant .NET Core et .NET 5+, les développeurs qui s’appuyaient sur .NET Framework pouvaient créer des services Windows pour effectuer des tâches d’arrière-plan ou exécuter des processus longs. Cette fonctionnalité est toujours disponible et permet de créer des services Worker qui s’exécutent en tant que service Windows.

Dans ce tutoriel, vous apprendrez à :

  • Publiez une application Worker .NET en tant qu’exécutable de fichier unique.
  • Créez un service Windows.
  • Créez l’application BackgroundService en tant que service Windows.
  • Démarrez et arrêtez le service Windows.
  • Affichez les journaux des événements.
  • Supprimez le service Windows.

Conseil

Tout le code source d’exemple « Workers dans .NET » peut être téléchargé dans l’Explorateur d’exemples. Pour plus d’informations, consultez Parcourir les exemples de code : Workers dans .NET.

Important

L’installation du Kit de développement logiciel (SDK) .NET installe également Microsoft.NET.Sdk.Worker et le modèle Worker. En d’autres termes, après avoir installé le Kit de développement logiciel (SDK) .NET, vous pouvez créer un Worker à l’aide de la commande dotnet new worker. Si vous utilisez Visual Studio, le modèle est masqué jusqu’à ce que la charge de travail de développement web et ASP.NET facultative soit installée.

Prérequis

Création d'un projet

Pour créer un projet de service Worker avec Visual Studio, vous devez sélectionner Fichier>Nouveau>Projet.... Dans la boîte de dialogue Créer un projet, recherchez « Worker Service », puis sélectionnez Modèle Worker Service. Si vous préférez utiliser l’interface CLI .NET, ouvrez votre terminal favori dans un répertoire de travail. Exécutez la commande dotnet new et remplacez le <Project.Name> par le nom de projet souhaité.

dotnet new worker --name <Project.Name>

Pour plus d’informations sur la commande de projet de service new worker de l’interface CLI .NET, consultez dotnet new worker.

Conseil

Si vous utilisez Visual Studio Code, vous pouvez exécuter des commandes CLI .NET à partir du terminal intégré. Pour plus d’informations, consultez Visual Studio Code : Terminal intégré.

Installer le package NuGet

Pour interagir avec les services Windows natifs à partir d’implémentations IHostedService .NET, vous devez installer le package NuGet Microsoft.Extensions.Hosting.WindowsServices.

Pour l’installer à partir de Visual Studio, utilisez la boîte de dialogue Gérer les packages NuGet.... Recherchez « Microsoft.Extensions.Hosting.WindowsServices » et installez-le. Si vous préférez utiliser l’interface CLI .NET, exécutez la commande dotnet add package :

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

Pour plus d’informations sur la commande d’ajout de package de l’interface CLI .NET, consultez dotnet add package.

Une fois les packages ajoutés, votre fichier projet doit maintenant contenir les références de package suivantes :

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

Mettre à jour le fichier projet

Ce projet Worker utilise les types de référence nullables de C#. Pour les activer pour l’ensemble du projet, mettez à jour le fichier projet en conséquence :

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

Les modifications apportées au fichier projet précédent ajoutent le nœud <Nullable>enable<Nullable>. Pour plus d’informations, consultez Définition du contexte nullable.

Créer le service

Ajoutez une nouvelle classe au projet nommé JokeService.cs et remplacez son contenu par le code C# suivant :

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

Le code source du service joke précédent expose un seul élément de fonctionnalité, la méthode GetJoke. Il s’agit d’une méthode de retour string qui produit une blague de programmation aléatoire. Le champ _jokes délimité par la classe est utilisé pour stocker la liste des blagues. Une blague aléatoire est sélectionnée dans la liste et retournée.

Réécrire la classe Worker

Remplacez le Worker existant à partir du modèle par le code C# suivant, puis renommez le fichier en 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);
        }
    }
}

Dans le code précédent, le code JokeService est injecté avec un ILogger. Les deux sont disponibles pour la classe en tant que champs private readonly. Dans la méthode ExecuteAsync, le service de blague demande une blague et l’écrit dans l’enregistreur d’événements. Dans ce cas, l’enregistreur d’événements est implémenté par le journal des événements Windows – Microsoft.Extensions.Logging.EventLog.EventLogLogger. Les journaux sont écrits et disponibles pour l’affichage dans l’observateur d’événements.

Notes

Par défaut, la gravité du journal des événements est Warning. Cela peut être configuré, mais à des fins de démonstration, WindowsBackgroundService journalise la méthode d’extension LogWarning. Pour cibler spécifiquement le niveau EventLog, ajoutez une entrée dans appsettings.{Environment}.json ou fournissez une valeur EventLogSettings.Filter.

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

Pour plus d’informations sur la configuration des niveaux de journalisation, consultez Fournisseurs de journalisation dans .NET : Configurer Windows EventLog.

Réécrire la classe Program

Remplacez le contenu du fichier modèle Program.cs par le code C# suivant :

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

La méthode d’extension AddWindowsService configure l’application pour qu’elle fonctionne en tant que service Windows. Le nom du service est défini sur ".NET Joke Service". Le service hébergé est inscrit pour l’injection de dépendances.

Pour plus d’informations sur l’inscription de services, consultez Injection de dépendances dans .NET.

Publier l’application

Pour créer l’application Service Worker .NET en tant que service Windows, il est recommandé de publier l’application en tant qu’exécutable de fichier unique. Il est moins risqué d’avoir un exécutable autonome, car le système ne comporte pas de fichiers dépendants. Toutefois, vous pouvez choisir une autre modalité de publication, qui est parfaitement acceptable, tant que vous créez un fichier *.exe qui peut être ciblé par le Gestionnaire de contrôle des services Windows.

Important

Une autre approche de publication consiste à générer le fichier *.dll (au lieu d’un fichier *.exe) et lorsque vous installez l’application publiée à l’aide du Gestionnaire de contrôle des services Windows que vous déléguez à l’interface CLI .NET et transmettez la DLL. Pour plus d’informations, consultez CLI .NET : commande 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>

Les lignes mises en surbrillance précédentes du fichier projet définissent les comportements suivants :

  • <OutputType>exe</OutputType> : crée une application console.
  • <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile> : active la publication à fichier unique.
  • <RuntimeIdentifier>win-x64</RuntimeIdentifier> : spécifie le RID de win-x64.
  • <PlatformTarget>x64</PlatformTarget> : spécifie le processeur de plateforme cible de 64 bits.

Pour publier l’application à partir de Visual Studio, vous pouvez créer un profil de publication persistant. Le profil de publication est basé sur XML et possède l’extension de fichier .pubxml. Visual Studio utilise ce profil pour publier l’application implicitement, alors que si vous utilisez l’interface CLI .NET, vous devez spécifier explicitement le profil de publication pour l’utiliser.

Cliquez avec le bouton droit sur le projet dans l’Explorateur de solutions, puis sélectionnez Publier.... Sélectionnez ensuite Ajouter un profil de publication pour créer un profil. Dans la boîte de dialogue Publier, sélectionnez Dossier en tant que votre Cible.

The Visual Studio Publish dialog

Conservez l’Emplacement par défaut, puis sélectionnez Terminer. Une fois le profil créé, sélectionnez Afficher tous les paramètres et vérifiez vos Paramètres de profil.

The Visual Studio Profile settings

Vérifiez que les paramètres suivants sont spécifiés :

  • Mode de déploiement : Autonome
  • Produire un fichier unique : vérifié
  • Activer la compilation ReadyToRun : cochée
  • Découper les assemblys inutilisés (en préversion) : décoché

Enfin, sélectionnez Publier. L’application est compilée et le fichier .exe résultant est publié dans le répertoire de sortie /publish.

Vous pouvez également utiliser l’interface CLI .NET pour publier l’application :

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

Pour plus d’informations, consultez dotnet publish.

Important

Avec .NET 6, si vous tentez de déboguer l’application avec le paramètre <PublishSingleFile>true</PublishSingleFile>, vous ne pourrez pas la déboguer. Pour plus d’informations, consultez Impossible d’attacher à CoreCLR lors du débogage d’une application « PublishSingleFile » .NET 6.

Créer le service Windows

Si vous ne savez pas utiliser PowerShell et que vous préférez créer un programme d’installation pour votre service, consultez Création d’un programme d’installation de service Windows. Sinon, utilisez la commande de création native du Gestionnaire de contrôle des services Windows (sc.exe) pour créer le service Windows. Exécutez PowerShell ISE en tant qu’administrateur.

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

Conseil

Si vous devez modifier la racine de contenu de la configuration de l’hôte, vous pouvez la passer en tant qu’argument de ligne de commande lors de la spécification de binpath :

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

Un message de sortie s’affiche :

[SC] CreateService SUCCESS

Pour plus d’informations, consultez sc.exe create.

Configurer le service Windows

Une fois le service créé, vous pouvez éventuellement le configurer. Si vous utilisez correctement les valeurs par défaut du service, passez à la section Vérifier la fonctionnalité de service.

Les services Windows fournissent des options de configuration de récupération. Vous pouvez interroger la configuration actuelle à l’aide de sc.exe qfailure "<Service Name>" (où <Service Name> correspond au nom de vos services) pour lire les valeurs de configuration de récupération actuelles :

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

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

La commande affiche la configuration de récupération, qui correspond aux valeurs par défaut, puisqu’elles n’ont pas encore été configurées.

The Windows Service recovery configuration properties dialog.

Pour configurer la récupération, utilisez sc.exe failure "<Service Name>"<Service Name> correspond au nom de votre service :

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

Conseil

Pour configurer les options de récupération, votre session de terminal doit s’exécuter en tant qu’administrateur.

Une fois qu’elle a été correctement configurée, vous pouvez interroger à nouveau les valeurs à l’aide de la commande 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.

Les valeurs de redémarrage configurées s’affichent alors.

The Windows Service recovery configuration properties dialog with restart enabled.

Options de récupération de service et instances .NET BackgroundService

Avec .NET 6, de nouveaux comportements de gestion des exceptions d’hébergement ont été ajoutés à .NET. L’énumération BackgroundServiceExceptionBehavior a été ajoutée à l’espace de noms Microsoft.Extensions.Hosting et permet de spécifier le comportement du service lorsqu’une exception est levée. Le tableau suivant répertorie les options disponibles :

Option Description
Ignore Ignorez les exceptions levées dans BackgroundService.
StopHost L’option IHost est arrêtée lorsqu’une exception non gérée est levée.

Le comportement par défaut est Ignore avant .NET 6, ce qui a entraîné des processus zombies (processus en cours d’exécution mais qui n’a rien fait). Avec .NET 6, StopHost est le comportement par défaut, ce qui entraîne l’arrêt de l’hôte lorsqu’une exception est levée. Cependant, elle s’arrête proprement, ce qui signifie que le système de management des services Windows ne redémarre pas le service. Pour autoriser correctement le redémarrage du service, vous pouvez appeler Environment.Exit avec un code de sortie différent de zéro. Tenez compte du bloc catch mis en surbrillance suivant :

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

Vérifier les fonctionnalités du service

Pour voir l’application créée en tant que service Windows, ouvrez Services. Sélectionnez la touche Windows (ou Ctrl + Échap), puis effectuez une recherche dans « Services ». À partir de l’application Services, vous devez être en mesure de trouver votre service par son nom.

Important

Par défaut, les utilisateurs standard (non-administrateurs) ne peuvent pas gérer les services Windows. Pour vérifier que cette application fonctionne comme prévu, vous devez utiliser un compte Administrateur.

The Services user interface.

Pour vérifier que le service fonctionne comme prévu, vous devez :

  • Démarrer le service
  • Afficher les journaux
  • Arrêter le service

Important

Pour déboguer l’application, vérifiez que vous ne tentez pas de déboguer l’exécutable qui s’exécute activement dans le processus des services Windows.

Unable to start program.

Démarrer le service Windows

Pour démarrer le service Windows, utilisez la commande sc.exe start suivante :

sc.exe start ".NET Joke Service"

Vous voyez une sortie similaire à ce qui suit :

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

L’état du service passe de START_PENDING à Running(En cours d’exécution).

Afficher les journaux d’activité

Pour afficher les journaux, ouvrez l’observateur d’événements. Sélectionnez la touche Windows (ou Ctrl + Échap), puis recherchez "Event Viewer". Sélectionnez le nœud Observateur d’événements (local)>Journaux Windows>Application. Vous devez voir une entrée de niveau Avertissement avec une source correspondant à l’espace de noms des applications. Double-cliquez sur l’entrée ou cliquez avec le bouton droit et sélectionnez Propriétés de l’événement pour afficher les détails.

The Event Properties dialog, with details logged from the service

Après avoir consulté les journaux dans le Journal des événements, vous devez arrêter le service. Il est conçu pour consigner une blague aléatoire une fois par minute. Il s’agit d’un comportement intentionnel mais qui n’est pas pratique pour les services de production.

Arrêter le service Windows

Pour arrêter le service Windows, utilisez la commande sc.exe stop suivante :

sc.exe stop ".NET Joke Service"

Vous voyez une sortie similaire à ce qui suit :

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

L’état du service passe de STOP_PENDING à Arrêté.

Supprimer le service Windows

Pour supprimer le service Windows, utilisez la commande de suppression native du Gestionnaire de contrôle de service Windows (sc.exe). Exécutez PowerShell ISE en tant qu’administrateur.

Important

Si le service n’est pas dans l’état Arrêté, il ne sera pas immédiatement supprimé. Vérifiez que le service est arrêté avant d’émettre la commande delete.

sc.exe delete ".NET Joke Service"

Un message de sortie s’affiche :

[SC] DeleteService SUCCESS

Pour plus d’informations, consultez sc.exe delete.

Voir aussi

Suivant