Back-End

9 mai, 2019

Novidades do .NET Core 3.0: Worker Services

Publicidade

A possibilidade de implementar em .NET Core rotinas de processamento em background (como Windows Services e Linux Daemons) é uma questão bastante recorrente entre desenvolvedores desta plataforma.

Em um artigo publicado há algum tempo eu abordei essa questão, através de uma solução que fazia uso de uma Console Application criada com o .NET Core 2.0 e Docker:

Agora, com o .NET Core 3.0, temos uma grande novidade: um template chamado Worker Service, que simplifica muito a implementação de aplicações que serão executadas como processos.

Neste artigo exploro este novo recurso, através da implementação de uma rotina de monitoramento de sites, baseada no exemplo do artigo já mencionado aqui (na época empreguei uma Console Application).

Criando um projeto Worker Service

Para os testes descritos neste artigo, utilizei a versão 16.1.0 Preview 2.0 do Visual Studio 2019.

A seguir, temos a nova tela de criação de projetos do Visual Studio 2019, sendo necessário selecionar para a criação da aplicação o template ASP.NET Core Web Application:

Preencher, na sequência, o nome do projeto (MonitoramentoSites, para este exemplo):

E concluir esse processo selecionando o template Worker Service:

Na próxima imagem é possível observar a estrutura desse novo tipo de projeto, com os arquivos appsettings.json, Program.cs e Worker.cs:

Um Worker Service também pode ser criado via linha de comando, através da instrução dotnet new:

Implementando o Worker Service

O primeiro ajuste envolvendo a implementação do Worker Server será no arquivo appsettings.json, através da inclusão da seção ServiceConfigurations:

  • O item Hosts conterá os domínios/sites a serem verificados
  • Em Intervalo, foi especificado o tempo em milissegundos a serem aguardados para a execução de cada verificação
{
  "ServiceConfigurations": {
    "Hosts": [ "siteinvalido.com.br", "github.com", "stackoverflow.com" ],
    "Intervalo": 30000
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Já a classe ServiceConfigurations será empregada na leitura das configurações associadas ao item de mesmo nome em appsettings.json:

namespace MonitoramentoSites
{
    public class ServiceConfigurations
    {
        public string[] Hosts { get; set; }
        public int Intervalo { get; set; }
    }
}

O tipo ResultadoMonitoramento conterá informações dos testes envolvendo a disponibilidade de sites/domínios:

namespace MonitoramentoSites
{
    public class ResultadoMonitoramento
    {
        public string Horario { get; set; }
        public string Host { get; set; }
        public string Status { get; set; }
        public object Exception { get; set; }
    }
}

O package Microsoft.AspNetCore.Mvc.NewtonsoftJson também será adicionado ao projeto MonitoramentoSites, a fim de permitir o uso de recursos de serialização no padrão JSON:

A classe Program não passará por alterações. Na próxima listagem está o código que corresponde a esse tipo, com a classe Worker sendo configurada para execução em CreateHostBuilder:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace MonitoramentoSites
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices(services =>
                {
                    services.AddHostedService<Worker>();
                });
    }
}

Ao criar um novo projeto, a classe Worker virá por default com um construtor, recebendo como parâmetro uma instância de ILogger, além do método ExecuteAsync com um loop para execução (a condição a ser verificada envolve uma checagem do valor associado à propriedade IsCancellationRequested no objeto stoppingToken).

A próxima listagem já contempla as alterações esperadas para este tipo:

  • O construtor receberá também uma instância de IConfiguration, que será empregada no preenchimento de um objeto do tipo ServiceConfigurations
  • Em ExecuteAsync, os diferentes domínios serão verificados a partir do uso de instâncias da classe Ping (namespace System.Net.NetworkInformation). O resultado desses testes serão então atribuídos a objetos baseados no tipo ResultadoMonitoramento
  • Os métodos LogInformation e LogError serão acionados por meio da instância vinculada a _logger, com a exibição em tela dos resultados dos testes de verificação
using System;
using System.Net.NetworkInformation;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;

namespace MonitoramentoSites
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly ServiceConfigurations _serviceConfigurations;

        public Worker(ILogger<Worker> logger,
            IConfiguration configuration)
        {
            _logger = logger;

            _serviceConfigurations = new ServiceConfigurations();
            new ConfigureFromConfigurationOptions<ServiceConfigurations>(
                configuration.GetSection("ServiceConfigurations"))
                    .Configure(_serviceConfigurations);
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker executando em: {time}",
                    DateTimeOffset.Now);

                foreach (string host in _serviceConfigurations.Hosts)
                {
                    _logger.LogInformation(
                        $"Verificando a disponibilidade do host {host}");

                    var resultado = new ResultadoMonitoramento();
                    resultado.Horario =
                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                    resultado.Host = host;

                    // Verifica a disponibilidade efetuando um ping
                    // no host que foi configurado em appsettings.json
                    try
                    {
                        using (Ping p = new Ping())
                        {
                            var resposta = p.Send(host);
                            resultado.Status = resposta.Status.ToString();
                        }
                    }
                    catch (Exception ex)
                    {
                        resultado.Status = "Exception";
                        resultado.Exception = ex;
                    }

                    string jsonResultado =
                        JsonConvert.SerializeObject(resultado);
                    if (resultado.Exception == null)
                        _logger.LogInformation(jsonResultado);
                    else
                        _logger.LogError(jsonResultado);
                }

                await Task.Delay(
                    _serviceConfigurations.Intervalo, stoppingToken);
            }
        }
    }
}

Testes

A seguir temos duas execuções dos testes de verificação acionados pela aplicação MonitoramentoSites:

Referências