Compartilhar via


Criar o Serviço Windows usando BackgroundService

Os desenvolvedores do .NET Framework provavelmente estão familiarizados com aplicativos do Serviço Windows. Antes do .NET Core e do .NET 5+, os desenvolvedores que contavam com o .NET Framework podiam criar os Serviços do Windows para executar tarefas em segundo plano ou executar processos de execução longa. Essa funcionalidade ainda está disponível e você pode criar Serviços de Trabalho que são executados como um Serviço windows.

Neste tutorial, você aprenderá a:

  • Publique um aplicativo de trabalho do .NET como um único executável de arquivo.
  • Criar um Serviço do Windows.
  • Crie o aplicativo BackgroundService como um Serviço Windows.
  • Inicie e interrompa o Serviço Windows.
  • Exibir logs de eventos.
  • Exclua o Serviço Windows.

Dica

Todo o código-fonte de exemplo de "Workers in .NET" está disponível no Navegador de Exemplos para download. Para obter mais informações, confira Procurar exemplos de código: Trabalhos no .NET.

Importante

Instalar o SDK do .NET também instala o Microsoft.NET.Sdk.Worker e o modelo de trabalho. Em outras palavras, depois de instalar o SDK do .NET, você pode criar um novo worker usando o comando dotnet new worker. Se você estiver usando o Visual Studio, o modelo ficará oculto até que o ASP.NET opcional e a carga de trabalho de desenvolvimento da Web sejam instalados.

Pré-requisitos

Criar um novo projeto

Para criar um projeto do Serviço de Trabalho com o Visual Studio, selecione Arquivo>Novo>Projeto.... Na caixa de diálogo Criar um projeto, pesquise "Serviço de Trabalho" e selecione o modelo de Serviço de Trabalho. Se você preferir usar a CLI do .NET, abra seu terminal favorito em um diretório de trabalho. Execute o comando dotnet new e substitua o <Project.Name> pelo nome do projeto desejado.

dotnet new worker --name <Project.Name>

Para obter mais informações sobre o novo comando do projeto de serviço de trabalho da CLI do .NET, confira dotnet new worker.

Dica

Se você estiver usando o Visual Studio Code, poderá executar comandos da CLI do .NET no terminal integrado. Para obter mais informações, consulte Visual Studio Code: Terminal Integrado.

Instalar o pacote NuGet

Para interoperar com os Serviços Windows nativos de implementações do .NET IHostedService, você precisará instalar o pacote NuGet Microsoft.Extensions.Hosting.WindowsServices.

Para instalar isso no Visual Studio, use a caixa de diálogo Gerenciar Pacotes NuGet. Pesquise por "Microsoft.Extensions.Hosting.WindowsServices" e instale-o. Se preferir usar a CLI do .NET, execute o seguinte comando. (Se você estiver usando uma versão SDK do .NET 9 ou anterior, use o formulário dotnet add package.)

dotnet package add Microsoft.Extensions.Hosting.WindowsServices

Para obter mais informações, consulte adição de pacote dotnet.

Depois de adicionar os pacotes com êxito, o arquivo de projeto agora deve conter as seguintes referências de pacote:

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

Atualizar arquivo de projeto

Este projeto de trabalho usa os tipos de referência anuláveis do C#. Para habilitá-los para todo o projeto, atualize o arquivo de projeto de acordo:

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
  </PropertyGroup>

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

As alterações de arquivo de projeto anteriores adicionam o nó <Nullable>enable<Nullable>. Para obter mais informações, consulte Configurando o contexto anulável.

Criar o serviço

Adicione uma nova classe ao projeto chamado JokeService.cse substitua seu conteúdo pelo seguinte código C#:

namespace App.WindowsService;

public sealed class JokeService
{
    public string GetJoke()
    {
        Joke joke = _jokes.ElementAt(
            Random.Shared.Next(_jokes.Count));

        return $"{joke.Setup}{Environment.NewLine}{joke.Punchline}";
    }

    // Programming jokes borrowed from:
    // https://github.com/eklavyadev/karljoke/blob/main/source/jokes.json
    private readonly HashSet<Joke> _jokes = new()
    {
        new Joke("What's the best thing about a Boolean?", "Even if you're wrong, you're only off by a bit."),
        new Joke("What's the object-oriented way to become wealthy?", "Inheritance"),
        new Joke("Why did the programmer quit their job?", "Because they didn't get arrays."),
        new Joke("Why do programmers always mix up Halloween and Christmas?", "Because Oct 31 == Dec 25"),
        new Joke("How many programmers does it take to change a lightbulb?", "None that's a hardware problem"),
        new Joke("If you put a million monkeys at a million keyboards, one of them will eventually write a Java program", "the rest of them will write Perl"),
        new Joke("['hip', 'hip']", "(hip hip array)"),
        new Joke("To understand what recursion is...", "You must first understand what recursion is"),
        new Joke("There are 10 types of people in this world...", "Those who understand binary and those who don't"),
        new Joke("Which song would an exception sing?", "Can't catch me - Avicii"),
        new Joke("Why do Java programmers wear glasses?", "Because they don't C#"),
        new Joke("How do you check if a webpage is HTML5?", "Try it out on Internet Explorer"),
        new Joke("A user interface is like a joke.", "If you have to explain it then it is not that good."),
        new Joke("I was gonna tell you a joke about UDP...", "...but you might not get it."),
        new Joke("The punchline often arrives before the set-up.", "Do you know the problem with UDP jokes?"),
        new Joke("Why do C# and Java developers keep breaking their keyboards?", "Because they use a strongly typed language."),
        new Joke("Knock-knock.", "A race condition. Who is there?"),
        new Joke("What's the best part about TCP jokes?", "I get to keep telling them until you get them."),
        new Joke("A programmer puts two glasses on their bedside table before going to sleep.", "A full one, in case they gets thirsty, and an empty one, in case they don’t."),
        new Joke("There are 10 kinds of people in this world.", "Those who understand binary, those who don't, and those who weren't expecting a base 3 joke."),
        new Joke("What did the router say to the doctor?", "It hurts when IP."),
        new Joke("An IPv6 packet is walking out of the house.", "He goes nowhere."),
        new Joke("3 SQL statements walk into a NoSQL bar. Soon, they walk out", "They couldn't find a table.")
    };
}

readonly record struct Joke(string Setup, string Punchline);

O código-fonte do serviço de piada anterior expõe uma única parte da funcionalidade, o método GetJoke. Este é um método de retorno string que representa uma piada de programação aleatória. O campo _jokes com escopo de classe é usado para armazenar a lista de piadas. Uma piada aleatória é selecionada na lista e retornada.

Reescrever a classe Worker

Substitua o Worker existente do modelo pelo seguinte código C# e renomeie o arquivo para 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);
        }
    }
}

No código anterior, o JokeService é injetado junto com um ILogger. Ambos são disponibilizados para a classe como campos . No método ExecuteAsync, o serviço de piada solicita uma piada e grava-a no registrador. Nesse caso, o agente é implementado pelo Log de Eventos do Windows – Microsoft.Extensions.Logging.EventLog.EventLogLoggerProvider. Os logs são gravados e estão disponíveis para exibição no Visualizador de Eventos.

Nota

Por padrão, a gravidade do Log de Eventos é Warning. Ela pode ser configurada, mas para fins de demonstração, o WindowsBackgroundService registra com o método de extensão LogWarning. Para direcionar especificamente ao nível EventLog, adicione uma entrada em appsettings.{Environment}.json ou informe um valor EventLogSettings.Filter.

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

Para obter mais informações sobre como configurar níveis de log, consulte Provedores de log no .NET: Configurar o EventLog do Windows.

Reescrever a classe Program

Substitua o conteúdo do arquivo Program.cs pelo seguinte código C#:

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

O método de extensão AddWindowsService configura o aplicativo para funcionar como um Serviço windows. O nome do serviço está definido como ".NET Joke Service". O serviço hospedado é registrado para injeção de dependência.

Para obter mais informações para registrar serviços, confira Injeção de dependência no .NET.

Publicar o aplicativo

Para criar o aplicativo .NET Worker Service como um Serviço windows, é recomendável que você publique o aplicativo como um único executável de arquivo. É menos propenso a erros ter um executável autocontido, pois não há arquivos dependentes espalhados pelo sistema de arquivos. Mas você pode escolher uma modalidade de publicação diferente, que é perfeitamente aceitável, desde que você crie um arquivo *.exe que possa ser direcionado pelo Gerenciador de Controle de Serviços do Windows.

Importante

Uma abordagem de publicação alternativa é criar o *.dll (em vez de um *.exe) e, quando você instala o aplicativo publicado usando o Gerenciador de Controle de Serviços do Windows, você delega à CLI do .NET e passa a DLL. Para obter mais informações, consulte .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.6" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.6" />
  </ItemGroup>
</Project>

As linhas realçadas anteriores do arquivo de projeto definem os seguintes comportamentos:

  • <OutputType>exe</OutputType>: cria um aplicativo de console.
  • <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>: habilita a publicação de arquivo único.
  • <RuntimeIdentifier>win-x64</RuntimeIdentifier>: Especifica o RID de win-x64.
  • <PlatformTarget>x64</PlatformTarget>: Especificar a CPU de 64 bits da plataforma de destino.

Para publicar o aplicativo no Visual Studio, você pode criar um perfil de publicação persistente. O perfil de publicação é baseado em XML e tem a extensão de arquivo .pubxml. O Visual Studio usa esse perfil para publicar o aplicativo implicitamente, enquanto se você estiver usando a CLI do .NET, deverá especificar explicitamente o perfil de publicação para que ele seja usado.

No Gerenciador de Soluções , clique com o botão direito do mouse no projetoe selecione Publicar. Em seguida, selecione Adicionar um perfil de publicação para criar um perfil. Na caixa de diálogo Publicar , selecione Pasta como destino.

O diálogo de publicação do Visual Studio

Deixe o Local padrão e selecione Concluir. Depois que o perfil for criado, selecione Mostrar todas as configuraçõese verifique as configurações do seu perfil .

as configurações do Perfil do Visual Studio

Verifique se as seguintes configurações estão especificadas:

  • Modo de implantação: autossuficiente
  • Produza arquivo único: verificado
  • Habilitar a compilação ReadyToRun: marcado como verificado
  • Cortar assemblies não usados (em versão prévia): não verificado

Por fim, selecione Publicar. O aplicativo é compilado e o arquivo de .exe resultante é publicado no diretório de saída /publish.

Como alternativa, você pode usar a CLI do .NET para publicar o aplicativo:

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

Para obter mais informações, consulte dotnet publish.

Importante

Com o .NET 6, se você tentar depurar o aplicativo com a configuração de <PublishSingleFile>true</PublishSingleFile>, não poderá depurar o aplicativo. Para obter mais informações, consulte Não é possível anexar ao CoreCLR ao depurar um aplicativo .NET 6 'PublishSingleFile'.

Criar o Serviço Windows

Se você não estiver familiarizado com o uso do PowerShell e preferir criar um instalador para seu serviço, consulte Criar um instalador do Serviço Windows. Caso contrário, para criar o Serviço Windows, use o comando de criação nativo do Gerenciador de Controle de Serviços do Windows (sc.exe). Execute o PowerShell como administrador.

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

Dica

Se você precisar mudar a raiz de conteúdo na configuração do host , poderá passá-la como argumento de linha de comando ao definir o binpath.

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

Você verá uma mensagem de saída:

[SC] CreateService SUCCESS

Para obter mais informações, consulte sc.exe create.

Configurar o Serviço Windows

Depois que o serviço for criado, você poderá configurá-lo opcionalmente. Se você aceitar os padrões de serviço, vá para a seção Verificar funcionalidade do serviço .

Os Serviços do Windows fornecem opções de configuração de recuperação. Você pode consultar a configuração atual usando o comando sc.exe qfailure "<Service Name>" (em que <Service Name> é o nome dos serviços) para ler os valores atuais de configuração de recuperação:

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

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

O comando produzirá a configuração de recuperação, que são os valores padrão, já que eles ainda não foram configurados.

O diálogo das propriedades de configuração de recuperação do Serviço do Windows.

Para configurar a recuperação, use a sc.exe failure "<Service Name>" em que <Service Name> é o nome do serviço:

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

Dica

Para configurar as opções de recuperação, sua sessão de terminal precisa ser executada como administrador.

Depois de configurado com êxito, você pode consultar os valores mais uma vez usando o 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.

Você verá os valores de reinicialização configurados.

O diálogo das propriedades de recuperação do Serviço do Windows com a reinicialização ativada.

Opções de recuperação de serviço e instâncias BackgroundService do .NET

Com o .NET 6, novos comportamentos de tratamento de exceção de hospedagem foram adicionados ao .NET. O enumerador BackgroundServiceExceptionBehavior foi adicionado ao namespace Microsoft.Extensions.Hosting e é usado para especificar o comportamento do serviço quando uma exceção é lançada. A tabela a seguir lista as opções disponíveis:

Opção Descrição
Ignore Ignorar exceções lançadas em BackgroundService.
StopHost A origem IHost será interrompida quando uma exceção sem tratamento for lançada.

O comportamento padrão antes do .NET 6 era Ignore, o que resultou em processos zumbis (um processo que estava em execução, mas que não realizava nenhuma ação). Com o .NET 6, o comportamento padrão é StopHost, o que faz com que o host seja interrompido quando uma exceção é gerada. Mas ele para de forma limpa, o que significa que o sistema de gerenciamento do Serviço Windows não reiniciará o serviço. Para permitir que o serviço seja reiniciado corretamente, você pode chamar Environment.Exit com um código de saída diferente de zero. Considere o bloco catch destacado a seguir.

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

Verificar a funcionalidade do serviço

Para ver o aplicativo criado como um Serviço windows, abra Services. Selecione a chave do Windows (ou Ctrl + Esc) e pesquise em "Serviços". No aplicativo Serviços, você deve ser capaz de localizar o seu serviço pelo nome.

Importante

Por padrão, os usuários regulares (não administradores) não podem gerenciar os serviços do Windows. Para verificar se esse aplicativo funciona conforme o esperado, você precisará usar uma conta de Administrador.

a interface do usuário dos Serviços.

Para verificar se o serviço está funcionando conforme o esperado, você precisa:

  • Iniciar o serviço
  • Exibir os logs
  • Parar o serviço

Importante

Para depurar o aplicativo, verifique se você não está tentando depurar o executável em execução ativamente no processo dos Serviços windows.

Não é possível iniciar o programa.

Iniciar o Serviço Windows

Para iniciar o Serviço Windows, use o comando sc.exe start:

sc.exe start ".NET Joke Service"

Você verá uma saída semelhante ao seguinte:

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

O Status do serviço fará a transição de START_PENDING para Em execução.

Exibir logs

Para exibir logs, abra o Visualizador de Eventos. Selecione a chave do Windows (ou Ctrl + Esc) e pesquise "Event Viewer". Selecione o nó Visualizador de Eventos (Local)>Logs do Windows>Aplicativo. Você deverá ver uma entrada no nível de Aviso com uma Origem que corresponda ao namespace de aplicativos. Clique duas vezes na entrada ou clique com o botão direito do mouse e selecione Propriedades do Evento para exibir os detalhes.

O diálogo Propriedades do Evento, com detalhes registrados do serviço

Depois de ver os logs no Log de Eventos, você deve interromper o serviço. Ele foi projetado para registrar uma piada aleatória uma vez por minuto. Esse é um comportamento intencional, mas não é prático para serviços de produção.

Parar o Serviço Windows

Para interromper o Serviço Windows, use o comando sc.exe stop:

sc.exe stop ".NET Joke Service"

Você verá uma saída semelhante ao seguinte:

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

O Status do serviço fará a transição de STOP_PENDING para Parado.

Excluir o Serviço Windows

Para excluir o Serviço Windows, use o comando de exclusão nativo do Windows Service Control Manager (sc.exe). Execute o PowerShell como administrador.

Importante

Se o serviço não estiver no estado Parado, ele não será excluído imediatamente. Verifique se o serviço foi interrompido antes de emitir o comando delete.

sc.exe delete ".NET Joke Service"

Você verá uma mensagem de saída:

[SC] DeleteService SUCCESS

Para obter mais informações, consulte sc.exe excluir.

Consulte também

Próximo