Quando se pensa em uma aplicação ASP.NET Core, provavelmente irá pensar em uma aplicação MVC, Web API ou Razor. Mas um detalhe pouco conhecido é que também é possível criar tarefas de segundo plano com o ASP.NET Core, através do conceito de generic hosts. Na versão 3.0 do .NET Core, este tipo de aplicação foi definida em um novo template chamado Worker Service, facilitando assim a criação das tarefas de segundo plano e dando mais destaque para este recurso.
O conceito de generic host foi introduzido na versão 2.1 do .NET Core e trata-se de uma aplicação ASP.NET Core que não processa requisições HTTP. O objetivo deste tipo de aplicação é remover o pipeline HTTP enquanto mantém os demais recursos de uma aplicação ASP.NET Core, como configuração, injeção de dependência e logging. Permitindo assim a criação de aplicações que precisam ser executadas em segundo plano, como serviços de mensagens.
Curso C# (C Sharp) Básico
Conhecer o cursoCriando uma aplicação Worker Service
Para criar uma aplicação Worker Service é necessário ter instalado o .NET Core 3.0, que no momento da criação deste artigo está no preview 7. Caso esteja utilizando o Visual Studio, este tipo de aplicação está disponível a partir da versão 2019.
Como estou utilizando o Visual Studio Code, vou criar a aplicação pela linha de comando com o código abaixo:
dotnet new worker -n WorkerSample
No Visual Studio 2019 você pode encontrar o template desta aplicação dentro dos subtemplates do ASP.NET Core.
Na classe Program
, podemos notar que é criado um servidor web:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
Que define um serviço, a classe Worker
, que veremos a seguir.
Definindo a tarefa
A classe responsável por executar a tarefa em segundo plano é a classe Worker
:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
Note que o construtor recebe uma instância de Logger
. Podemos utilizar esta instância para notificar o usuário do estado do serviço. Já no método ExecuteAsync
é onde a tarefa de segundo plano é executada.
Note que ela é executada enquanto o serviço não for cancelado.
Para este artigo iremos modificar apenas o método ExecuteAsync
, adicionando informações sobre o estado do serviço:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("O serviço está iniciando.");
stoppingToken.Register(() => _logger.LogInformation("Tarefa de segundo plano está parando."));
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Executando tarefa: {time}", DateTimeOffset.Now);
await Task.Delay(10000, stoppingToken);
}
_logger.LogInformation("O serviço está parando.");
}
Ao executar a aplicação temos o resultado abaixo:
No momento ela ainda não está sendo executada como serviço, vamos configurá-la para isso.
Curso C# (C Sharp) Intermediário
Conhecer o cursoConfigurando a tarefa como serviço para ser executada em segundo plano
Para executar a aplicação como serviço, é necessário adicionar o pacote “Microsoft.Extensions.Hosting.WindowsServices
” na aplicação:
dotnet add package Microsoft.Extensions.Hosting.WindowsServices
Ao adicioná-lo podemos chamar o método UseWindowsService()
:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
Este método verifica se a aplicação está executando como serviço e faz as configurações necessárias de acordo deste contexto. Ela também configura a aplicação para utilizar o ServiceBaseLifetime
, que auxilia no controle do ciclo de vida da aplicação enquanto ela é executada como serviço. Isso sobrescreve o padrão ConsoleLifetime
.
Publicando o serviço
Com o ServiceBaseLifetime
configurado, podemos publicar a nossa aplicação:
dotnet publish -c Release
O comando acima irá publicar a aplicação de acordo com o ambiente de desenvolvimento. Você também pode publicar a aplicação para um sistema específico com a opção -r
:
dotnet publish -c Release -r <RID>
`` significa Runtime Identifier (RID) e na documentação você pode ver as opções disponíveis.
Instalando o serviço no Windows
Com a aplicação publicada, ela pode ser instalada como serviço no Windows com o utilitário sc:
sc create workersample binPath=C:\Programs\Services\WorkerSample.exe
Instalando o serviço no Mac OS X
No caso do Mac OS X, os serviços são conhecimentos como daemon. Para definir o nosso serviço neste sistema como um daemon, é necessário possuir o .NET Core instalado na máquina e definir um script:
#!bin/bash
#Start WorkerSample if not running
if [ “$(ps -ef | grep -v grep | grep WorkerSample | wc -l)” -le 0 ]
then
dotnet /usr/local/services/WorkerSample.dll
echo "WorkerSampler Started"
else
echo "WorkerSample Already Running"
fi
Dê para este script permissão de execução:
chmod +x ~/scripts/startup/startup.sh
Os daemons neste sistema operacional são definidos na pasta /Library/LaunchDaemons/
, onde deve ser configurado em um arquivo XML com a extensão .plist
(não faz parte do escopo deste artigo explicar as configurações deste arquivo, você pode conhecê-las na documentação do launchd).
No nosso caso, o arquivo terá o conteúdo abaixo:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:</string>
</dict>
<key>Label</key>
<string>br.com.treinaweb.workersample</string>
<key>Program</key>
<string>/Users/admin/scripts/startup/startup.sh</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>LaunchOnlyOnce</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/startup.stdout</string>
<key>StandardErrorPath</key>
<string>/tmp/startup.stderr</string>
<key>UserName</key>
<string>admin</string>
<key>GroupName</key>
<string>admin</string>
<key>InitGroups</key>
<true/>
</dict>
</plist>
Com isso, para iniciar o daemon basta carregar o arquivo .plist
, que acabamos de definir:
sudo launchctl load -w /Library/LaunchDaemons/br.com.treinaweb.workersample.plist
Instalando o daemon no Linux
Assim como no Mac OS X, no Linux os serviços são conhecidos como daemon e nesta plataforma também é necessário possuir o .NET Core instalado na máquina. Fora isso, cada distribuição pode definir uma forma de criação de daemon, aqui vou explicar a criação utilizando o systemd.
No systemd o serviço é definido em um arquivo .service
salvo em /etc/systemd/system
e que no exemplo deste artigo conterá o conteúdo o abaixo:
[Unit]
Description=Dotnet Core Demo service
[Service]
ExecStart=/bin/dotnet/dotnet WorkerSample.dll
WorkingDirectory=/usr/local/services
User=dotnetuser
Group=dotnetuser
Restart=on-failure
SyslogIdentifier=dotnet-sample-service
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Após isso, basta habilitar o daemon:
systemctl enable worker-sample.service
Que ele poderá ser iniciado:
systemctl start dotnet-sample-service.service
Obs: Infelizmente no Mac OS X e Linux, a aplicação não sabe quando está sendo parada, algo que ocorre no Windows, quando ela é executada como serviço.
Curso C# (C Sharp) Avançado
Conhecer o cursoO Worker Service é bom?
O template Worker Service é uma boa adição para o .NET Core, pois isso pode tornar os generic hosts cada vez mais populares. Permitindo que as pessoas consigam utilizar os recursos do ASP.NET Core em uma aplicação console.
E com o tempo, espero que melhore o suporte da aplicação como daemon em ambientes Unix.