Agendando e-mails no .NET com HangFire
O Hangfire é uma biblioteca open source para .NET que executa tarefas em segundo plano com persistência, e por isso resolve muito bem o agendamento de e-mails. Imagine que você precise enviar milhares de mensagens para seus usuários sem travar a aplicação web e sem depender de uma rotina manual.
A resposta para esse cenário são os background jobs. Neste artigo, vamos explorar os conceitos fundamentais da biblioteca usando um único caso prático: um agendador de e-mails construído com ASP.NET Core e Minimal APIs.
O problema que o Hangfire resolve
Suponha um sistema que permite agendar campanhas de e-mail. Cada mensagem precisa sair na data e na hora definidas, mesmo que o servidor esteja sob carga ou seja reiniciado no meio do caminho.
Além disso, o time quer acompanhar o status de cada envio. Afinal, saber que uma campanha falhou só quando o cliente reclama não é uma opção aceitável.
Uma saída ingênua seria disparar o envio com Task.Run dentro do próprio endpoint. No entanto, essa tarefa vive apenas na memória do processo, então um deploy ou uma queda do servidor apaga tudo o que estava pendente.
Também seria possível escrever um serviço hospedado com BackgroundService, como mostramos no artigo sobre Background Services em C# e .NET. Contudo, nesse caso você mesmo precisaria implementar fila, persistência, novas tentativas e monitoramento.
O Hangfire entrega exatamente essas peças prontas. Por isso, ele dispensa serviços adicionais como Windows Services, o Agendador de Tarefas do Windows ou cron jobs externos.
O que é o Hangfire
O Hangfire é uma biblioteca que permite executar métodos .NET em background com confiabilidade. Segundo o repositório oficial do projeto, ela é distribuída sob a licença LGPLv3 e funciona em aplicações web, aplicações de console e serviços do Windows.
Na prática, a biblioteca oferece suporte a quatro modelos principais de execução:
- Jobs imediatos, também chamados de fire-and-forget.
- Agendamentos para um momento futuro.
- Tarefas recorrentes, baseadas em expressões CRON.
- Continuações, que encadeiam um job ao término de outro.
O ponto central da arquitetura é o armazenamento persistente. O Hangfire grava cada job em um storage, como SQL Server ou Redis, e por isso as tarefas não se perdem quando o processo cai.
Existem ainda pacotes mantidos pela comunidade para PostgreSQL e outros bancos. Dessa forma, você escolhe o storage que já faz parte da sua infraestrutura.
Como o Hangfire processa os jobs
O fluxo tem três participantes. Primeiro, o cliente cria o job e o serializa no storage; em seguida, o servidor do Hangfire busca os jobs pendentes e os executa em threads próprias.
Por fim, o resultado de cada execução volta para o storage como um novo estado. Assim, um job passa por estados como Enqueued, Scheduled, Processing, Succeeded e Failed, e você consegue consultar esse histórico a qualquer momento.
Essa separação também permite escalar. Você pode ter várias instâncias da API criando jobs e um ou mais servidores processando a fila, todos compartilhando o mesmo storage.
Configurando o Hangfire no projeto
Vamos criar uma aplicação ASP.NET Core com um endpoint que agenda e-mails. O primeiro passo é instalar os pacotes NuGet necessários.
dotnet add package Hangfire
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.MemoryStorage
O pacote Hangfire traz o núcleo da biblioteca e Hangfire.AspNetCore faz a integração com o host e com a injeção de dependência. Já Hangfire.MemoryStorage guarda os jobs em memória, o que simplifica o exemplo.
Para produção, prefira um banco como SQL Server ou PostgreSQL. Em memória, os jobs desaparecem quando a aplicação reinicia, ou seja, você perde justamente a garantia que motivou o uso do Hangfire.
Vale citar também o pacote Hangfire.InMemory, publicado pela própria equipe do Hangfire. Ele serve ao mesmo propósito de desenvolvimento e testes, então você pode adotá-lo no lugar do Hangfire.MemoryStorage se preferir um pacote oficial.
Registrando os serviços do Hangfire
Toda a configuração acontece no Program.cs. Vamos montar o arquivo em partes, começando pelos using e pela criação do builder.
using Hangfire;
using Hangfire.MemoryStorage;
var builder = WebApplication.CreateBuilder(args);
Os using importam as dependências do Hangfire. Enquanto isso, o builder prepara o container de serviços e a configuração da aplicação.
builder.Services.AddHangfire(config =>
config.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseMemoryStorage());
Aqui adicionamos o Hangfire aos serviços da aplicação. Cada chamada da cadeia tem um papel específico:
SetDataCompatibilityLeveldefine o formato dos dados gravados no storage.UseSimpleAssemblyNameTypeSerializergrava nomes de tipos mais curtos, sem versão do assembly.UseRecommendedSerializerSettingsaplica as configurações de serialização JSON recomendadas.UseMemoryStorageescolhe o armazenamento em memória para testes e desenvolvimento.
O nível de compatibilidade merece atenção, porque ele dita como os dados ficam no banco. Por isso, ao atualizar a biblioteca, revise as notas da versão antes de subir esse valor.
builder.Services.AddHangfireServer();
builder.Services.AddSingleton<IEmailService, EmailService>();
O método AddHangfireServer registra o servidor que processa os jobs em background como um serviço hospedado. Logo em seguida, registramos nosso serviço de e-mail na injeção de dependência.
Ativando o dashboard e os endpoints
Com os serviços registrados, construímos a aplicação e ativamos o painel de monitoramento.
var app = builder.Build();
app.UseHangfireDashboard("/hangfire");
app.MapGet("/", () => "Email scheduling API with Hangfire");
O dashboard fica acessível em /hangfire. Além dele, criamos uma rota de teste simples que apenas retorna uma mensagem padrão.
app.MapPost("/schedule-email", (EmailRequest request, IBackgroundJobClient client) =>
{
// Agenda o envio para o horário informado pelo cliente
client.Schedule<IEmailService>(
x => x.SendEmailAsync(request.To, request.Subject, request.Body),
request.ScheduledAt - DateTime.UtcNow);
return Results.Ok("Email scheduled successfully!");
});
app.Run();
Esse endpoint recebe a requisição e usa client.Schedule para registrar a execução futura de SendEmailAsync. Então o Hangfire grava o job no estado Scheduled e só o enfileira quando o horário chega.
Repare que o segundo argumento é um TimeSpan calculado a partir de ScheduledAt. A biblioteca também aceita um DateTimeOffset direto, o que evita a conta e deixa a intenção mais clara.
Exemplo completo da API com Hangfire
Juntando todas as partes, o Program.cs fica assim:
using Hangfire;
using Hangfire.MemoryStorage;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHangfire(config =>
config.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseMemoryStorage());
builder.Services.AddHangfireServer();
builder.Services.AddSingleton<IEmailService, EmailService>();
var app = builder.Build();
app.UseHangfireDashboard("/hangfire");
app.MapGet("/", () => "Email scheduling API with Hangfire");
app.MapPost("/schedule-email", (EmailRequest request, IBackgroundJobClient client) =>
{
client.Schedule<IEmailService>(
x => x.SendEmailAsync(request.To, request.Subject, request.Body),
request.ScheduledAt - DateTime.UtcNow);
return Results.Ok("Email scheduled successfully!");
});
app.Run();
Criando o serviço de envio de e-mail
Agora definimos uma interface e uma implementação concreta para o serviço que envia os e-mails. Essa separação entre contrato e implementação facilita a injeção de dependência e os testes unitários.
Além disso, ela permite trocar o mecanismo de envio no futuro. Por exemplo, você pode substituir o Console.WriteLine por um provedor como SendGrid ou por um cliente SMTP sem mexer no agendamento.
Interface do serviço
public interface IEmailService
{
Task SendEmailAsync(string to, string subject, string body);
}
A interface IEmailService define um contrato com um único método, SendEmailAsync. Ele recebe o destinatário, o assunto e o corpo do e-mail.
Assim, qualquer classe que implemente essa interface segue o mesmo formato. Isso também ajuda o Hangfire, porque o job aponta para o contrato e não para uma classe específica.
Implementação concreta
public class EmailService : IEmailService
{
public async Task SendEmailAsync(string to, string subject, string body)
{
Console.WriteLine($"[Email sent] To: {to} | Subject: {subject}");
await Task.Delay(1000); // simula o tempo de envio
}
}
A classe EmailService implementa IEmailService e simula o envio escrevendo no console. Em seguida, ela aguarda um segundo com Task.Delay(1000) para imitar a latência de um provedor real.
Em uma aplicação de verdade, esse método chamaria uma API de envio de e-mails. Mesmo assim, o design orientado a interfaces continua valendo, pois facilita a manutenção, permite usar mocks em testes e desacopla o código das implementações específicas.
O DTO EmailRequest
A classe EmailRequest é um DTO, ou seja, um objeto que transporta dados entre a camada da API e a lógica de negócio. Neste contexto, ela representa o JSON que o cliente envia para agendar um e-mail.
public class EmailRequest
{
public string To { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public DateTime ScheduledAt { get; set; }
}
O endpoint /schedule-email espera esses quatro campos. Cada um tem uma função clara:
Toguarda o endereço de e-mail do destinatário.Subjectguarda o título do e-mail.Bodyguarda o conteúdo da mensagem.ScheduledAtindica a data e a hora, em UTC, do envio.
Com esses dados, o serviço sabe quem recebe, o que recebe e quando recebe. Portanto, o DTO concentra toda a informação que o Hangfire precisa serializar junto com o job.
Um cuidado importante é manter os argumentos pequenos. O Hangfire grava os parâmetros do método no storage, então prefira passar identificadores e buscar os dados completos dentro do job.
Monitorando jobs no dashboard do Hangfire
O Hangfire inclui um painel visual acessível em /hangfire. Com ele, você consegue acompanhar o ciclo de vida de cada envio sem escrever uma linha de código extra.
Entre as funções mais úteis do painel, destacam-se estas:
- Ver jobs agendados, em execução, concluídos ou com erro.
- Reprocessar falhas manualmente com um clique.
- Acompanhar o histórico de execução e os detalhes de cada exceção.
- Disparar jobs recorrentes sob demanda.
Por padrão, o dashboard só aceita requisições locais. Por isso, ele funciona no seu localhost durante o desenvolvimento, mas bloqueia o acesso quando a aplicação vai para um servidor.
Para liberar o acesso em produção, você precisa configurar uma regra de autorização. Uma abordagem comum é mapear o painel com MapHangfireDashboard e aplicar RequireAuthorization com uma política que exija um usuário autenticado e com o papel adequado.
Nunca exponha o painel sem proteção. Afinal, ele permite reexecutar e excluir jobs, e também revela os argumentos de cada chamada.
Tipos de jobs do Hangfire
Além do Schedule, o Hangfire suporta outros formatos de execução. Cada um atende a uma necessidade diferente do agendador de e-mails.
Enqueueexecuta o job assim que um worker estiver livre.Scheduleexecuta o job em um momento futuro.RecurringJob.AddOrUpdatecria jobs recorrentes, como um relatório todo dia às 8h.ContinueJobWithencadeia um job ao término de outro.
Um e-mail de boas-vindas, por exemplo, pode usar Enqueue. Assim, o cadastro responde na hora e o envio acontece logo depois, fora da requisição.
Jobs recorrentes
O exemplo abaixo cria um relatório diário enviado ao administrador:
RecurringJob.AddOrUpdate<IEmailService>(
"daily-report",
x => x.SendEmailAsync("admin@site.com", "Daily report", "Content..."),
Cron.Daily);
O primeiro argumento é o identificador do job recorrente. Com ele, você atualiza ou remove a agenda depois, e o painel exibe esse nome na aba de recorrentes.
A classe Cron oferece atalhos como Daily, Hourly e Weekly. Também é possível passar uma expressão CRON completa quando a agenda é mais específica.
Continuações
As continuações são úteis quando uma etapa depende da anterior. Por exemplo, você pode gerar um relatório e, só depois que ele terminar, enviar o e-mail com o link.
var reportJobId = BackgroundJob.Enqueue<IReportService>(x => x.GenerateAsync());
BackgroundJob.ContinueJobWith<IEmailService>(
reportJobId,
x => x.SendEmailAsync("admin@site.com", "Report ready", "Your report is ready."));
Nesse caso, o segundo job só entra na fila quando o primeiro termina com sucesso. Dessa forma, o fluxo fica explícito e rastreável no dashboard.
Falhas e novas tentativas automáticas
Provedores de e-mail falham com frequência por limites de taxa, instabilidade de rede ou indisponibilidade momentânea. Felizmente, a biblioteca trata esse cenário de forma nativa.
Segundo a documentação sobre exceções, o filtro AutomaticRetry vem aplicado globalmente com 10 tentativas. Além disso, o intervalo entre as tentativas cresce a cada falha.
Quando as tentativas se esgotam, o job vai para o estado Failed e aparece no painel. Lá ele permanece até alguém excluí-lo, então você pode corrigir a causa e reprocessar manualmente.
Você também pode ajustar esse comportamento por método. Para desativar as novas tentativas em um job, basta decorar o método com [AutomaticRetry(Attempts = 0)].
Como um job pode rodar mais de uma vez, escreva o envio de forma idempotente. Na prática, registre que o e-mail saiu e verifique esse registro antes de enviar de novo.
Boas práticas para usar o Hangfire em produção
O exemplo deste artigo usa memória para facilitar o aprendizado. Em produção, porém, alguns cuidados fazem diferença:
- Use um storage persistente, como SQL Server, PostgreSQL ou Redis.
- Proteja o dashboard com autenticação e autorização.
- Passe identificadores como argumentos, e não objetos grandes.
- Trabalhe sempre com datas em UTC ao agendar.
- Escreva jobs idempotentes, porque novas tentativas acontecem.
Também vale lembrar que o Hangfire resolve dependências pelo container do ASP.NET Core. Portanto, serviços com tempo de vida scoped funcionam dentro dos jobs, já que cada execução recebe seu próprio escopo.
Se o volume crescer, você pode separar o processamento em outro processo. Nesse caso, a API apenas cria os jobs, enquanto um worker dedicado chama AddHangfireServer e consome a fila.
Hangfire ou outras opções de agendamento
O Hangfire não é a única forma de agendar tarefas no .NET. O próprio framework oferece os serviços hospedados, descritos na documentação de hosted services do ASP.NET Core, que atendem bem tarefas simples e contínuas.
Outra alternativa popular é o Quartz.NET, focado em agendas complexas e calendários. Mostramos essa biblioteca em detalhes no artigo Agendando tarefas com Quartz no .NET.
Em resumo, o Hangfire se destaca quando você quer disparar jobs a partir do código da aplicação, com persistência e painel prontos. Já o Quartz brilha em cenários dominados por agendas e gatilhos.
Considerações finais
Com o Hangfire, você desacopla tarefas demoradas do fluxo principal da aplicação. Como resultado, o sistema fica mais rápido para o usuário, mais confiável diante de falhas e mais fácil de monitorar.
Usando um agendador de e-mails, mostramos como configurar a biblioteca, agendar envios e acompanhar tudo pelo dashboard. Agora, o próximo passo é trocar o storage em memória por um banco real e proteger o painel antes de ir para produção.
FAQ
O Hangfire é gratuito?
Sim, o núcleo do Hangfire é open source e distribuído sob a licença LGPLv3. Existem extensões comerciais, mas o agendamento básico não exige licença paga.
Posso usar o Hangfire com PostgreSQL?
Sim, existem pacotes de storage mantidos pela comunidade para PostgreSQL. Basta trocar UseMemoryStorage pelo método de configuração do pacote escolhido.
O que acontece com os jobs se a aplicação reiniciar?
Com um storage persistente, os jobs continuam gravados e voltam a ser processados quando o servidor sobe. Com armazenamento em memória, eles se perdem.
Quantas vezes o Hangfire tenta executar um job que falhou?
Por padrão, são 10 tentativas com intervalos crescentes. Você pode alterar esse número com o atributo AutomaticRetry.
Como proteger o dashboard em produção?
Configure uma política de autorização e aplique ao endpoint do painel. Sem isso, o dashboard aceita apenas requisições locais.