Executando processamentos em segundo plano no .NET com o Hangfire

O Hangfire é uma biblioteca para .NET que executa processamentos em segundo plano a partir de filas e agendamentos, sem depender do Agendador de Tarefas do Windows. Se você já desenvolveu projetos de integração entre sistemas, provavelmente já dedicou algumas horas para montar uma estrutura capaz de executar rotinas a partir de um agendamento.

Neste artigo, você conhece o Hangfire na prática, com base no site oficial da biblioteca, desde a instalação até o dashboard. Além disso, ele ajuda em cenários bem comuns do dia a dia:

  • Disparo automatizado de e-mails;
  • Importação em lote de arquivos XML, CSV e JSON;
  • Limpeza rotineira de arquivos temporários;
  • Emissão automatizada de relatórios recorrentes.

O que é o Hangfire

O Hangfire é uma biblioteca open source, gratuita inclusive para uso comercial, distribuída pelo NuGet. Ela facilita a criação, a execução e o gerenciamento de jobs em segundo plano.

Em outras palavras, a biblioteca cuida de operações que precisam rodar fora do pipeline de processamento de requisições da sua aplicação. Assim, a requisição HTTP responde rápido, enquanto o trabalho pesado acontece depois.

Na prática, um job do Hangfire nada mais é do que a chamada a um método da sua própria solução. Você descreve essa chamada com uma expressão lambda, e a biblioteca guarda essa descrição para executar no momento certo.

Como o Hangfire funciona por dentro

A arquitetura da biblioteca tem três peças principais. Primeiro, o cliente cria os jobs; em seguida, o storage persiste esses jobs; por fim, o servidor busca e executa cada um deles.

Quando você chama o método Enqueue, o cliente serializa as informações do método e todos os seus argumentos. Depois disso, ele grava o job no storage e o adiciona a uma fila.

Do outro lado, as threads de trabalho do servidor seguem um ciclo simples. Elas buscam o próximo job, escondem esse job dos demais workers, executam o método e só então removem o item da fila.

Por isso, o Hangfire oferece uma garantia de execução do tipo "pelo menos uma vez". Se o processo cair no meio de um job, a biblioteca devolve esse job à fila, conforme o mecanismo de cada storage.

Essa garantia tem um efeito importante no seu código. Como um mesmo job pode rodar mais de uma vez, os métodos executados em segundo plano devem tolerar repetição sem gerar efeitos duplicados.

Requisitos para usar o Hangfire

Você pode utilizar o Hangfire em praticamente qualquer tipo de projeto .NET. Por exemplo, ele funciona em um Console Application, em uma aplicação web com ASP.NET ou em uma aplicação Windows Forms.

Neste artigo, a demonstração usa uma aplicação web com ASP.NET Core. Quanto às plataformas suportadas, a biblioteca exige uma destas opções:

  • .NET Framework 4.5 ou superior;
  • .NET Core 1.0 ou posterior;
  • Qualquer plataforma compatível com .NET Standard 1.3.

Além da plataforma, você também precisa de um storage persistente. Sem ele, o servidor não teria onde buscar os jobs criados pelo cliente.

Instalando o Hangfire no projeto

Você adiciona o Hangfire ao projeto a partir dos pacotes distribuídos no NuGet. Ao pesquisar pelo nome da biblioteca, você encontra diversas extensões que fornecem recursos extras ou implementam novas abstrações.

Este artigo não entra no detalhe de cada extensão, mas vale saber que elas existem. Caso precise, a página de extensões da documentação traz a relação completa com uma visão geral de cada uma.

O básico para tudo funcionar está no pacote Hangfire.Core. Então, se a sua solução usa ASP.NET Core, instale também o pacote de integração correspondente:

dotnet add package Hangfire.Core
dotnet add package Hangfire.AspNetCore

Escolhendo o storage

O storage é o lugar onde o Hangfire guarda jobs, filas, estados e histórico. A documentação oficial usa o SQL Server como exemplo principal, mas a comunidade mantém opções para Redis, PostgreSQL, MongoDB e outros bancos.

Neste exemplo, a escolha recai sobre o SQLite, porque ele não exige nenhum servidor de banco de dados instalado. Você encontra a lista de storages disponíveis na seção de storages da documentação.

dotnet add package Hangfire.Storage.SQLite

Para estudos e demonstrações, o SQLite resolve muito bem. Em produção, contudo, prefira um storage robusto e compartilhado, principalmente quando várias instâncias da aplicação processam a mesma fila.

Configurando o Hangfire na aplicação

Depois de instalar os pacotes, o primeiro passo é registrar o serviço da biblioteca no container de injeção de dependências. Para isso, adicione as linhas abaixo ao método ConfigureServices da classe Startup:

services.AddHangfire(configuration => configuration
    .UseRecommendedSerializerSettings()
    .UseSQLiteStorage());

// Define a quantidade de retentativas aplicadas a um job com falha.
// O padrão é 10; aqui reduzimos para 3, com intervalo de 5 minutos.
GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute
{
    Attempts = 3,
    DelaysInSeconds = new int[] { 300 }
});

services.AddHangfireServer();

O método AddHangfire define a configuração global, como o serializador e o storage. Já o AddHangfireServer registra o servidor que processa os jobs dentro do próprio processo da aplicação.

A documentação oficial também mostra outras opções de configuração, como SetDataCompatibilityLevel e UseSimpleAssemblyNameTypeSerializer. Elas ajudam a manter a compatibilidade dos dados gravados entre versões da biblioteca.

Retentativas automáticas

Por padrão, o Hangfire aplica o filtro AutomaticRetry a todos os métodos, com dez tentativas. Quando um job falha, a biblioteca agenda uma nova execução com atraso crescente entre as tentativas.

No código acima, o filtro global reduz esse número para três tentativas, com cinco minutos de intervalo. Se todas as tentativas falharem, o job vai para o estado de falha, e você pode executá-lo de novo manualmente.

Você também pode desligar as retentativas em um método específico. Basta decorar o método com o atributo correspondente:

[AutomaticRetry(Attempts = 0)]
public void SendReport()
{
    // Este método não será repetido automaticamente em caso de falha
}

Habilitando o dashboard

Em seguida, adicione o dashboard, que permite visualizar e gerenciar os jobs configurados. Essa etapa é opcional, mas muito útil, então adicione o trecho abaixo ao método Configure da classe Startup:

app.UseHangfireDashboard();

Por padrão, o dashboard responde no caminho /hangfire e aceita apenas requisições locais. Mais adiante, você vê como liberar o acesso remoto de forma segura.

Tipos de jobs no Hangfire

Com o Hangfire, você cria diversos tipos de tarefas. Em resumo, todas elas executam métodos implementados na sua solução, mas cada tipo resolve um momento diferente de execução.

Jobs fire-and-forget

Como o próprio nome diz, esse tipo de job roda apenas uma vez e depois é "esquecido". Mesmo assim, o dashboard guarda o histórico, então você ainda consegue disparar o job manualmente sempre que quiser.

Para criar um job fire-and-forget, utilize o método Enqueue da classe BackgroundJob:

var jobId = BackgroundJob.Enqueue(
    () => Console.WriteLine("Fire-and-forget job!"));

O método devolve o identificador do job. Por exemplo, você pode guardar esse valor para consultar o estado da execução ou para encadear outro job depois.

Jobs delayed

Esse tipo de job também roda apenas uma vez, mas não de imediato. Nesse caso, você define um atraso para a execução, que pode ser de dias, horas ou minutos.

Para criar um job delayed, utilize o método Schedule da classe BackgroundJob:

BackgroundJob.Schedule(
    () => Console.WriteLine("Delayed job: 2 minutes after startup"),
    TimeSpan.FromMinutes(2));

Esse formato combina bem com tarefas como lembretes, confirmações tardias ou o cancelamento automático de um pedido que não recebeu pagamento.

Recurring jobs

Os jobs recorrentes executam a mesma tarefa muitas vezes, seguindo um agendamento no formato CRON. Para criar um job recorrente, utilize o método AddOrUpdate da classe RecurringJob:

RecurringJob.AddOrUpdate(
    "my-recurring-job",
    () => Console.WriteLine((new Random().Next(1, 200) % 2 == 0)
        ? "Recurring job generated an even number"
        : "Recurring job generated an odd number"),
    Cron.Minutely,
    TimeZoneInfo.Local);

O primeiro argumento é o identificador do job recorrente, e ele precisa ser único. Caso contrário, você acaba com um único job, porque cada chamada com o mesmo identificador atualiza o registro existente.

A classe Cron traz atalhos para intervalos comuns, como minuto, hora, dia, semana, mês e ano. Para agendas mais complexas, você também pode passar uma expressão CRON diretamente como texto.

Além disso, a classe RecurringJob oferece o método RemoveIfExists, que remove o job sem lançar exceção quando ele não existe. Já o método Trigger dispara o job na hora, sem alterar o próximo horário agendado.

Jobs com continuations

As continuações definem que a execução de um job depende do término de um job pai. Para criar um job com continuação, utilize o método ContinueJobWith da classe BackgroundJob:

var jobId = BackgroundJob.Enqueue(
    () => Console.WriteLine("Parent fire-and-forget job!"));

BackgroundJob.ContinueJobWith(
    jobId,
    () => Console.WriteLine($"Continuation job! (Parent job: {jobId})"));

Esse recurso ajuda a montar pequenos fluxos de trabalho. Por exemplo, primeiro você importa um arquivo e, logo depois, envia um e-mail avisando que a importação terminou.

Usando serviços da aplicação nos jobs do Hangfire

Os exemplos anteriores usam Console.WriteLine para simplificar, mas um sistema real chama serviços da aplicação. Nesse caso, o Hangfire aceita a versão genérica dos métodos e resolve o tipo pelo container de dependências.

public interface IReportService
{
    void GenerateMonthlyReport(int month, int year);
}

// O Hangfire resolve IReportService pelo container no momento da execução
BackgroundJob.Enqueue<IReportService>(
    service => service.GenerateMonthlyReport(10, 2023));

Em um controller, você também pode injetar a interface IBackgroundJobClient em vez de usar a classe estática. Assim, o código fica mais fácil de testar, porque você substitui o cliente por um dublê nos testes.

public class ReportsController : ControllerBase
{
    private readonly IBackgroundJobClient _jobs;

    public ReportsController(IBackgroundJobClient jobs) => _jobs = jobs;

    [HttpPost("reports")]
    public IActionResult Create()
    {
        var jobId = _jobs.Enqueue<IReportService>(
            service => service.GenerateMonthlyReport(10, 2023));

        return Accepted(new { jobId });
    }
}

Repare que o endpoint devolve 202 Accepted com o identificador do job. Dessa forma, o cliente sabe que o pedido entrou na fila, mesmo que o relatório ainda não esteja pronto.

Executando a demonstração com Hangfire

O código fonte da aplicação de demonstração está disponível no GitHub. Com tudo que você fez até aqui, já dá para executar a aplicação e ver o Hangfire em ação.

Para isso, execute o comando abaixo a partir da pasta do projeto:

dotnet run

Assim que a aplicação sobe, o terminal mostra algumas informações sobre o Hangfire e os logs gerados pelos jobs de disparo imediato:

Terminal exibindo os logs de inicialização do Hangfire e as mensagens dos jobs executados pela aplicação
Logs do terminal logo após a aplicação de demonstração iniciar.

Acessando o dashboard do Hangfire

O dashboard do Hangfire fica disponível na URL http://localhost:5000/hangfire. Logo na abertura, ele exibe um painel de monitoramento dos jobs executados.

Navegando pelas seções, você encontra funcionalidades para gerenciar a execução dos jobs:

Dashboard do Hangfire com o gráfico de monitoramento de jobs e o menu de seções
Painel inicial com o monitoramento em tempo real das execuções.
  • Tarefas: lista os jobs em execução, agendados ou do histórico, de acordo com o status. Para executar de novo um job do histórico, basta selecioná-lo e clicar em "Tarefas enfileiradas".
  • Retentativas: reúne os jobs que falharam e aguardam uma nova tentativa, conforme a configuração de retentativas aplicada no início.
  • Tarefas recorrentes: mostra a lista de jobs recorrentes da aplicação. Você também pode selecionar um ou vários itens e dispará-los manualmente.
  • Servidores: exibe os servidores do Hangfire que estão rodando e processando as filas.

Protegendo o dashboard em produção

Como vimos, o dashboard aceita apenas requisições locais por padrão. Isso protege o painel, mas também impede o acesso quando a aplicação roda em um servidor remoto.

Para liberar o acesso de forma controlada, implemente a interface IDashboardAuthorizationFilter. O exemplo abaixo permite a entrada apenas de usuários autenticados com o papel de administrador:

public class AdminAuthorizationFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();

        // Somente usuários autenticados com o papel Admin acessam o painel
        return httpContext.User.Identity?.IsAuthenticated == true
            && httpContext.User.IsInRole("Admin");
    }
}

Depois, informe o filtro nas opções do dashboard. Lembre também de chamar UseHangfireDashboard depois dos middlewares de autenticação, porque o filtro depende do usuário já identificado:

app.UseAuthentication();
app.UseAuthorization();

app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = new[] { new AdminAuthorizationFilter() }
});

Quando você registra vários filtros, o acesso só acontece se todos devolverem verdadeiro. Além disso, a propriedade IsReadOnlyFunc coloca o painel em modo somente leitura para determinados usuários.

Boas práticas com Hangfire

Alguns cuidados simples evitam boa parte dos problemas em produção. Primeiro, passe argumentos pequenos para os jobs, como identificadores, em vez de objetos inteiros.

Isso importa porque o Hangfire serializa os argumentos e grava tudo no storage. Assim, o job busca os dados atualizados no momento da execução, e o storage não cresce sem necessidade.

Em seguida, escreva métodos idempotentes. Como a biblioteca garante execução pelo menos uma vez, o mesmo e-mail não deve sair duas vezes só porque o processo reiniciou no meio do job.

Também mantenha o servidor sempre ativo. A documentação lembra que o servidor do Hangfire precisa estar no ar para agendar e processar os jobs, algo que exige atenção em aplicações hospedadas no IIS.

Por fim, registre logs dentro dos jobs. O artigo sobre ILogger no ASP.NET mostra como estruturar esses registros, e eles ajudam muito quando uma retentativa acontece de madrugada.

Hangfire, hosted services ou Quartz

O ASP.NET Core já traz uma forma nativa de rodar tarefas em segundo plano: os hosted services. A documentação da Microsoft mostra como implementar a interface IHostedService ou herdar da classe BackgroundService.

Essa abordagem funciona bem para laços contínuos e tarefas simples com timer. No entanto, ela não oferece persistência, retentativas, histórico nem painel de monitoramento prontos.

O Hangfire entrega exatamente essas peças, então ele brilha quando os jobs precisam sobreviver a reinícios e ser acompanhados por alguém. Se você quiser comparar as abordagens, veja o artigo sobre background services em C# e .NET.

Outra alternativa popular é o Quartz.NET, focado em agendamentos com calendários e gatilhos sofisticados. Para conhecer essa opção, leia o artigo sobre agendamento de tarefas com Quartz no .NET.

Conclusão

O Hangfire é uma solução completa e robusta, além de extensível e compatível com os mais diversos tipos de projetos do ecossistema .NET. Por isso, ele é uma excelente opção para criar tarefas em segundo plano sem se amarrar ao Agendador de Tarefas do Windows.

Neste artigo, você instalou a biblioteca, configurou o storage com SQLite e ajustou as retentativas. Além disso, criou jobs fire-and-forget, delayed, recorrentes e com continuações, e ainda aprendeu a proteger o dashboard.

O repositório do projeto de demonstração está disponível no GitHub: lucasdemoraesc/Demo.Hangfire. Então, clone o projeto e experimente criar seus próprios jobs.

Para continuar estudando, vale ler também sobre Health Check no ASP.NET, o Discord como repositório de log em .NET e o envio agendado de e-mails.

FAQ

O Hangfire é gratuito para uso comercial?

Sim. A biblioteca é open source e gratuita, inclusive para uso comercial, e existe também uma versão paga com recursos extras.

Preciso de banco de dados para usar o Hangfire?

Sim, você precisa de um storage persistente. Ele pode ser SQL Server, Redis, PostgreSQL, SQLite ou outro banco suportado por uma extensão.

Qual a diferença entre Enqueue e Schedule?

O método Enqueue coloca o job na fila para execução imediata. Já o Schedule executa o job uma única vez depois do atraso informado.

Quantas vezes o Hangfire repete um job com falha?

Por padrão, ele tenta dez vezes com atraso crescente. Você muda esse valor com o atributo AutomaticRetry, global ou por método.

Por que o dashboard não abre no servidor?

Por padrão, o painel aceita apenas requisições locais. Para acesso remoto, configure um filtro que implemente IDashboardAuthorizationFilter.