Criar serviço do Windows usando BackgroundService
Os desenvolvedores do .NET Framework provavelmente estão familiarizados com os aplicativos de serviço do Windows. Antes do .NET Core e do .NET 5+, os desenvolvedores que dependiam do .NET Framework podiam criar os Serviços do Windows para executar tarefas em segundo plano ou executar processos de longa execução. Essa funcionalidade ainda está disponível e você pode criar Serviços de Trabalho que são executados como um Serviço do Windows.
Neste tutorial, você aprenderá a:
- Publique um aplicativo de trabalho .NET como um único arquivo executável.
- Crie um serviço do Windows.
- Crie o aplicativo
BackgroundService
como um serviço do Windows. - Inicie e pare o Serviço do Windows.
- Exibir logs de eventos.
- Exclua o serviço do Windows.
Dica
Todo o código-fonte de exemplo "Workers in .NET" está disponível no Samples Browser para download. Para obter mais informações, consulte Procurar exemplos de código: Trabalhadores no .NET.
Importante
A instalação do 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 trabalhador usando o comando dotnet new worker. Se você estiver usando o Visual Studio, o modelo ficará oculto até que a carga de trabalho opcional de ASP.NET e desenvolvimento da Web seja instalada.
Pré-requisitos
- O SDK do .NET 8.0 ou posterior
- Um sistema operacional Windows
- Um ambiente de desenvolvimento integrado (IDE) .NET
- Sinta-se à vontade para usar Visual Studio
Criar um novo projeto
Para criar um novo projeto do Serviço de Trabalho com o Visual Studio, selecione Arquivo>Novo Projeto>.... Na caixa de diálogo Criar um novo projeto procure por "Serviço de Trabalhador" e selecione Modelo de Serviço de Trabalhador. 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 comando .NET CLI new worker service project, consulte dotnet new worker.
Dica
Se você estiver usando o Visual Studio Code, poderá executar comandos da CLI do .NET a partir do terminal integrado. Para obter mais informações, consulte Visual Studio Code: Integrated Terminal.
Instalar o pacote NuGet
Para interoperar com os Serviços nativos do Windows a partir de implementações do .NET IHostedService, você precisará instalar o pacote NuGet Microsoft.Extensions.Hosting.WindowsServices
.
Para instalar isso a partir do Visual Studio, use a caixa de diálogo Gerenciar Pacotes NuGet... Procure por "Microsoft.Extensions.Hosting.WindowsServices" e instale-o. Se preferir usar a CLI do .NET, execute o comando dotnet add package
:
dotnet add package Microsoft.Extensions.Hosting.WindowsServices
Para obter mais informações sobre o comando .NET CLI add package, consulte dotnet add package.
Depois de adicionar com êxito os pacotes, seu arquivo de projeto agora deve conter as seguintes referências de pacote:
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.2" />
</ItemGroup>
Atualizar arquivo de projeto
Este projeto de trabalho faz uso dos tipos de referência anuláveis 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.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.2" />
</ItemGroup>
</Project>
As alterações no ficheiro de projeto anterior adicionam o nó <Nullable>enable<Nullable>
. Para obter mais informações, consulte Definindo 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
de âmbito de classe é usado para armazenar a lista de piadas. Uma piada aleatória é selecionada da 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 juntamente com um ILogger
. Ambos são disponibilizados para a classe como campos. No método ExecuteAsync
, o serviço de anedotas solicita uma piada e escreve-a no registo. Neste caso, o registador é implementado pelo Registo de Eventos do Windows - Microsoft.Extensions.Logging.EventLog.EventLogLoggerProvider. Os logs são gravados e estão disponíveis para visualização no Visualizador de Eventos.
Observação
Por padrão, a gravidade do do Log de Eventos é Warning. Isso pode ser configurado, mas, para fins de demonstração, utiliza-se o método de extensão LogWarning para os logs de WindowsBackgroundService
. Para direcionar especificamente o nível de EventLog
, adicione uma entrada nas appsettings.{Environment}.jsonou forneça 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 registo, consulte fornecedores de registo no .NET: Configurar o Registro de Eventos do Windows.
Reescrever a classe Program
Substitua o conteúdo do arquivo de modelo 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 do Windows. O nome do serviço é definido como ".NET Joke Service"
. O serviço hospedado está registrado para injeção de dependência.
Para saber mais sobre o registo de serviços, consulte injeção de dependência no .NET.
Publicar o aplicativo
Para criar o aplicativo .NET Worker Service como um Serviço do Windows, é recomendável publicar o aplicativo como um único arquivo executável. É menos propenso a erros ter um executável independente, pois não há arquivos dependentes ao redor do sistema de arquivos. Mas você pode escolher uma modalidade de publicação diferente, o que é perfeitamente aceitável, desde que você crie um arquivo *.exe que pode 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, delega à CLI do .NET e passa a DLL. Para obter mais informações, consulte .NET CLI: dotnet command.
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.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.2" />
</ItemGroup>
</Project>
As linhas destacadas anteriores do arquivo de projeto definem os seguintes comportamentos:
-
<OutputType>exe</OutputType>
: Cria um aplicativo de console. -
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
: Permite a publicação de arquivo único. -
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
: Especifica o RID de dewin-x64
. -
<PlatformTarget>x64</PlatformTarget>
: Especifique a CPU da plataforma de destino de 64 bits.
Para publicar o aplicativo do 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.
Clique direito no projeto no Gerenciador de Soluçõese selecione Publicar. Em seguida, selecione Adicionar um perfil de publicação para criar um perfil. A partir da janela de diálogo Publicar , selecione Pasta como o seu Destino .
Deixe o de Localização depadrão e selecione Concluir. Depois de criar o perfil, selecione Mostrar todas as configuraçõese verifique as configurações do Perfil.
Certifique-se de que as seguintes configurações sejam especificadas:
- Modo de implantação: Autônomo
- Produzir arquivo único: verificado
- Ativar compilação ReadyToRun: verificado
- Aparar montagens não utilizadas (em pré-visualização): não verificado
Por fim, selecione Publicar. O aplicativo é compilado e o arquivo .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 <PublishSingleFile>true</PublishSingleFile>
, não poderá depurar o aplicativo. Para mais informações, consulte Não é possível anexar ao CoreCLR ao depurar uma aplicação .NET 6 'PublicarArquivoÚnico'.
Criar o serviço do 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 de serviço do Windows. Caso contrário, para criar o Serviço do Windows, use o comando create do Gerenciador de Controle de Serviços do Windows (sc.exe) nativo. Execute o PowerShell como administrador.
sc.exe create ".NET Joke Service" binpath= "C:\Path\To\App.WindowsService.exe"
Dica
Se precisar alterar a raiz do conteúdo do de configuração do host, você pode passá-lo como um argumento de linha de comando ao especificar 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 mais informações, consulte esc.exe para criar.
Configurar o serviço do Windows
Depois que o serviço for criado, você poderá configurá-lo opcionalmente. Se estiver bem com os padrões de serviço, vá para a secção Verificação da 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>"
(onde <Service Name>
é o nome dos serviços) para ler os valores atuais da 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.
Para configurar a recuperação, use o sc.exe failure "<Service Name>"
onde <Service Name>
é o nome do seu 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, a sessão do 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.
Opções de recuperação de serviço e instâncias BackgroundService
de .NET
Com o .NET 6, novos comportamentos de tratamento de exceções de hospedagem foram adicionados ao .NET. O enum 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 | Ignore as exceções lançadas em BackgroundService . |
StopHost | O IHost será interrompido quando uma exceção não tratada for lançada. |
O comportamento padrão antes do .NET 6 era Ignore
, o que resultou em processos zumbis (um processo em execução que não executa nenhuma tarefa). Com o .NET 6, o comportamento padrão é StopHost
, o que resulta na interrupção do host quando uma exceção é lançada. No entanto, ele para de forma limpa, o que significa que o sistema de gestão de serviços do Windows não reinicia 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 do Windows, abra o Services. Selecione a tecla Windows (ou Ctrl + Esc) e pesquise em "Serviços". A partir da aplicação Serviços, deverá conseguir encontrar o seu serviço pelo respetivo nome.
Importante
Por padrão, os usuários regulares (não administradores) não podem gerenciar os serviços do Windows. Para verificar se este aplicativo funciona conforme o esperado, você precisará usar uma conta de administrador.
Para verificar se o serviço está funcionando conforme o esperado, você precisa:
- Iniciar o serviço
- Ver os registos
- Parar o serviço
Importante
Para depurar o aplicativo, certifique-se de que você não está tentando depurar o executável que está sendo executado ativamente no processo de Serviços do Windows.
Iniciar o Serviço Windows
Para iniciar o Serviço do Windows, use o comando sc.exe start
:
sc.exe start ".NET Joke Service"
Você verá um resultado 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 serviço Status passará de START_PENDING
para Em execução.
Ver registros
Para exibir logs, abra o Visualizador de Eventos. Selecione a tecla Windows (ou Ctrl + Esc) e procure por "Event Viewer"
. Selecione o Visualizador de Eventos (Local)>Logs do Windows>nó Aplicação. Você deverá ver uma entrada de nível Aviso com uma origem correspondente ao namespace das aplicações. Clique duas vezes na entrada ou clique com o botão direito do mouse e selecione Propriedades do Evento para exibir os detalhes.
Depois de ver os logs no Log de Eventos , deve interromper o serviço. Ele foi projetado para registrar uma piada aleatória uma vez por minuto. Este é um comportamento intencional, mas não é prático para serviços de produção.
Pare o Serviço do Windows
Para parar o Serviço do Windows, use o comando sc.exe stop
:
sc.exe stop ".NET Joke Service"
Poderá obter uma saída semelhante à 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 serviço Status passará de STOP_PENDING
para Interrompido.
Excluir o serviço do Windows
Para excluir o Serviço do Windows, use o comando delete nativo do Gerenciador de Controle de Serviços do Windows (sc.exe). Execute o PowerShell como administrador.
Importante
Se o serviço não estiver no estado Interrompido, 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.
Ver também
- Criar um instalador de serviço do Windows
- Serviços de Worker no .NET
- Criar um serviço de fila
-
Usar serviços com escopo em um
BackgroundService
-
Implementar a interface
IHostedService