Um Hosted Service é uma classe com a lógica de uma tarefa que executa em segundo plano da aplicação e que implementa a interface
IHostedService
Permite executar tarefas que podem ser desacopladas do fluxo normal da funcionalidade, permitindo a funcionalidade focar na lógica de negócio e evitando gerenciar outras tarefas dentro do próprio fluxo. Alguns exemplos que podem ser desacoplados e movidos para um Hosted Service são reagir a eventos ou mensagens externas, processamentos de dados, etc.
Os tipos mais conhecidos de Hosted Service são a classe de ajuda BackgroundService
e o template Worker Service.
Antes de explicar o que é um Background Task e Worker Service é necessário entender sobre o que é um Host no contexto de Hosted Service.
Generic Host
É um objeto que encapsula os recursos de ciclo de vida de uma aplicação como:
- Injeção de Dependência
- Logging
- Configuração
- Implementações de
IHostedService
Uma das várias responsabilidades do Host é inicializar ou encerrar todos os Hosted Services registrados chamando IHostedService.StartAsync
ou IHostedService.StopAsync
.
Como o artigo não é sobre Generic Hosted não será aprofundado no tema nisso, mas caso tenha interesse em saber mais sobre o assunto segue o link sobre Generic Host
O exemplo abaixo é muito parecido com o que é utilizado no ASP.NET no momento de registrar serviços, logging, configuração etc.
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services)=>
{
services.AddSingleton<IWeatherApi, WeatherApi>();
services.Configure<MyExternalConfig>(context.Configuration.GetSection("MyExternalConfig")); // Configuration Options
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
IHostedService
Implementação
Tudo começa pela implementação dos métodos StartAsync
e StopAsync
da interface IHostedService
que está no pacote nuget Microsoft.Extensions.Hosting
.
StartAsync
Reage na inicialização da aplicação. É utilizado para alocar os recursos necessários para a execução da tarefa. Esses recursos podem ser de diversas formas como abrir conexões, preparar valores em variáveis que serão utilizados, alocar memória, etc.
StartAsync
deve ser de curta duração para não bloquear a inicialização de outras tarefas.
É importante que não bloqueie a chamada desse método porque caso isso aconteça a aplicação não será inicializada.
StopAsync
Reage no encerramento correto da aplicação, isso significa quando o encerramento pode ser controlado, mas caso a aplicação seja finalizada de forma inesperada o StopAsync
não será chamado. StopAsync
é utilizado para desalocar recursos ou fazer pequenas limpezas em locais ou dados que foram utilizados durante a tarefa, exemplos são limpeza de arquivos ou pastas, fechar conexões, desalocar memória, etc.
Se um erro for gerado durante a execução da tarefa em segundo plano, Dispose
deverá ser chamado mesmo se StopAsync
não for chamado. [Microsoft]
public class Worker : IHostedService, IDisposable
{
private readonly ILogger<Worker> logger;
public Worker(ILogger<Worker> logger)
{
this.logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Conexão com banco de dados foi aberta");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Conexão com banco de dados foi fechada");
return Task.CompletedTask;
}
public void Dispose()
{
logger.LogInformation($"Recursos foram descartados");
}
}
Tempo de execução no encerramento
StopAsync
tem um detalhe onde por padrão tem que ser executado em no máximo 5 segundos para indicar que teve sucesso no encerramento, mas caso isso não aconteça os serviços restantes não serão parados mesmo se ainda não tiverem concluído o processamento. Caso os serviços precisem de mais tempo para pararem pode alterar o tempo limite definindo um novo valor em ShutdownTimeout
nas opções do Host.
Na configuração dos serviços é definido o valor do tempo no ShutdownTimeout
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.Configure<HostOptions>(options =>
{
// Define um tempo maior para a finalização
options.ShutdownTimeout = TimeSpan.FromSeconds(40);
});
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
Background Task
É uma tarefa que é fisicamente acoplada e executada em segundo plano em uma aplicação. Por ser acoplada a uma aplicação, ela compartilha recursos e pode gerenciá-los para evitar que uma funcionalidade se preocupe com isso no seu próprio fluxo. Um exemplo é o cache de dados externos que a tarefa pode obter os dados e adicionar ao cache para outras funcionalidades da aplicação utilizarem. O ciclo de vida de um Background Task é o mesmo que o da aplicação que ela está acoplada.
Classe BackgroundService
BackgroundService
é uma classe abstrata que implementa IHostedService
Ela foca na execução de uma tarefa através do método ExecuteAsync
que é obrigatório implementar. Já os métodos StartAsync
e StopAsync
são encapsulados, permitindo escolher implementar apenas um ou outro caso precise, já que em alguns casos pode não ser importante controlar a inicialização(StartAsync
) ou encerramento(StopAsync
) de um Hosted Service.
Um Background Task não necessariamente precisa implementar a classe abstrata BackgroundService
ele pode ser qualquer classe que implemente IHostedService
e seja feito o deploy junto com a aplicação.
ExecuteAsync
É o local onde contém a lógica para executar a tarefa.
Como o ExecuteAsync
é chamado dentro de StartAsync
e ele também não pode bloquear a execução porque a aplicação não irá iniciar.
No exemplo abaixo é simulado o desacoplamento do fluxo de alguma funcionalidade a necessidade de fazer o trabalho que a tarefa está fazendo.
public class DadosExternosCacheService : BackgroundService
{
private readonly ILogger<DadosExternosCacheService> _logger;
// Injeta as dependências para a tarefa executar
public DadosExternosCacheService(ILogger<DadosExternosCacheService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Verifica se não houve cancelamento.
while (!stoppingToken.IsCancellationRequested)
{
// 1. Obtém os dados externos da api ou banco de dados;
// 2. Trata os dados;
// 3. Adicionar ao cache;
_logger.LogInformation("Dados externos atualizados: {time}", DateTimeOffset.Now);
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
Aplicabilidade
Exemplos de quando utilizar são:
- Responder a mensagens ou eventos externos;
- Executar tratamento de dados, fora do fluxo da requisição;
- Obter e tratar dados externos para adicionar ao cache para outras funcionalidades usarem;
- Agrupar dados por tempo, quantidade ou tamanho e enviar tudo de uma vez em uma requisição. (Batch request);
Cuidado ao adicionar muitos Background Tasks em um mesmo projeto porque isso pode significar que existe um problema na arquitetura do projeto onde tem muitas responsabilidades e talvez seja necessário utilizar Worker Process em uma abordagem no estilo micro-serviços para resolver o problema.
Worker Service
É uma aplicação Console com um Host que contém uma ou mais tarefas(implementações de IHostedService
que foram fisicamente desacopladas da aplicação principal. Essas tarefas por estarem desacopladas da aplicação principal tem o ciclo de vida diferente, onde são feitos os deploys, inicializações e encerramentos dos Hosted Services de forma independente. Um exemplo de como essa diferença ajuda é na forma do deploy que evita que caso o Worker Service precise ser alterado tenha que fazer um deploy da aplicação principal junto.
Para utilizar um Worker Service é necessário escolher o tipo “Worker Service” no momento de criar um novo projeto ou executar a linha de comando dotnet new worker -o NomeDoProjeto
Ao criar o projeto pode perceber que contém um exemplo chamado Worker.cs
ele é uma tarefa que implementa BackgroundService
.
Aplicabilidade
- Executar tarefas que podem ser desacopladas da aplicação principal;
- Executar tarefas agendadas;
- Reagir a mensagens ou eventos externos;
- Separar a responsábilidades para micro-serviços;
- Formatação e limpeza de dados para IA/ML;
Dicas sobre Hosted Services
Escopo (Injeção de Dependência)
Os Hosted Services são registrados como singleton, isso significa que eles que caso queira outro escopo além de singleton será necessário injetar IServiceProvider
para obter outras instâncias.
// Program.cs
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(6);
});
services.AddHostedService<Worker>();
services.AddTransient<IWeatherApi, WeatherApi>(); // Exemplo registrado como Transient
})
.Build();
await host.RunAsync();
// Worker
public class Worker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<Worker> _logger;
public Worker(IServiceProvider serviceProvider,
ILogger<Worker> logger) => (_serviceProvider, _logger) = (serviceProvider, logger);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _serviceProvider.CreateScope();
var weatherApi = scope.ServiceProvider.GetService<IWeatherApi>();
var result = await weatherApi.GetWeatherAsync(stoppingToken);
_logger.LogInformation($"Weather: {result}");
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
public interface IWeatherApi
{
Task<string> GetWeatherAsync(CancellationToken stoppingToken);
}
public class WeatherApi : IWeatherApi
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching","Fun"
};
public Task<string> GetWeatherAsync(CancellationToken stoppingToken)
{
var rng = new Random();
return Task.FromResult(Summaries[rng.Next(Summaries.Length)]);
}
}
Ordem de execução
Os serviços são inicializados na ordem que são registrados, mas são encerrados na ordem reversa.
Caso registre os serviços ServicoA, ServicoB e ServicoC. Durante a inicialização IHostedService.StartAsync
do ServicoA será executado primeiro, mas durante o encerramento o último serviço registrado será executado primeiro, no caso IHostedService.StopAsync
do ServicoC.
Worker Service como Windows Service ou Linux Daemon
Um Worker Service pode ser instalado como um Windows Service ou Linux Daemon apenas usando extension methods UseWindowsService
para Windows Service ou UseSystemd
para Linux Deamon no Host.
É necessário instalar o pacote nuget equivalente para o tipo de ambiente que vai executar;
- Windows Service:
Microsoft.Extensions.Hosting.WindowsService
- Linux Daemon:
Microsoft.Extensions.Hosting.Systemd
É possível usar UseWindowsService
e UseSystemd
juntos porque ambos só funcionam baseado no contexto do sistema operacional que estão preparados para funcionar.
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(6);
});
services.AddHostedService<Worker2>();
services.AddHostedService<Worker>();
})
.UseWindowsService() // Windows Service
.UseSystemd() // Linux Daemon
.Build();
await host.RunAsync();