Módulo 5 · Worker Service — Capítulo 06

Projeto Prático — Worker com Procedures

Fechando o módulo: um worker completo que processa pedidos pendentes a cada 5 minutos, juntando escopo de DI, agendamento, logging e as procedures do Módulo 2.

1. O cenário

Um pedido criado via API (Módulo 4) fica com status "Pendente" até que um processo de retaguarda o confirme — simulando, por exemplo, uma checagem de fraude assíncrona ou a integração com um sistema de pagamento externo. Este worker roda a cada 5 minutos, busca pedidos pendentes, "processa" cada um e atualiza o status via procedure.

2. A procedure de apoio

SQL usp_PedidoListarPendentes e usp_PedidoConfirmar
CREATE OR ALTER PROCEDURE dbo.usp_PedidoListarPendentes
AS
BEGIN
    SET NOCOUNT ON;
    SELECT Id, ClienteId, ValorTotal
    FROM dbo.Pedido
    WHERE Status = 'Pendente';
END;
GO

CREATE OR ALTER PROCEDURE dbo.usp_PedidoConfirmar
    @PedidoId INT
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    UPDATE dbo.Pedido
    SET Status = 'Confirmado'
    WHERE Id = @PedidoId AND Status = 'Pendente';
END;
GO

3. O Repository

C# IPedidoProcessamentoRepository
public interface IPedidoProcessamentoRepository
{
    Task<List<PedidoPendente>> ListarPendentesAsync();
    Task ConfirmarAsync(int pedidoId);
}

public record PedidoPendente(int Id, int ClienteId, decimal ValorTotal);

public class PedidoProcessamentoRepository : IPedidoProcessamentoRepository
{
    private readonly string _connectionString;
    public PedidoProcessamentoRepository(string connectionString) => _connectionString = connectionString;

    public async Task<List<PedidoPendente>> ListarPendentesAsync()
    {
        await using var conexao = new SqlConnection(_connectionString);
        var resultado = await conexao.QueryAsync<PedidoPendente>(
            "dbo.usp_PedidoListarPendentes", commandType: CommandType.StoredProcedure);
        return resultado.ToList();
    }

    public async Task ConfirmarAsync(int pedidoId)
    {
        await using var conexao = new SqlConnection(_connectionString);
        await conexao.ExecuteAsync("dbo.usp_PedidoConfirmar",
            new { PedidoId = pedidoId }, commandType: CommandType.StoredProcedure);
    }
}

4. O Worker completo

C# ProcessadorPedidosWorker.cs
public class ProcessadorPedidosWorker : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<ProcessadorPedidosWorker> _logger;
    private readonly WorkerOptions _opcoes;

    public ProcessadorPedidosWorker(
        IServiceScopeFactory scopeFactory,
        ILogger<ProcessadorPedidosWorker> logger,
        IOptions<WorkerOptions> opcoes)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
        _opcoes = opcoes.Value;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var temporizador = new PeriodicTimer(TimeSpan.FromMinutes(_opcoes.IntervaloEmMinutos));

        while (await temporizador.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                await ProcessarPendentesAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Falha ao processar pedidos pendentes");
            }
        }
    }

    private async Task ProcessarPendentesAsync(CancellationToken stoppingToken)
    {
        using var escopo = _scopeFactory.CreateScope();
        var repositorio = escopo.ServiceProvider.GetRequiredService<IPedidoProcessamentoRepository>();

        var pendentes = await repositorio.ListarPendentesAsync();
        _logger.LogInformation("{Total} pedidos pendentes encontrados", pendentes.Count);

        foreach (var pedido in pendentes)
        {
            stoppingToken.ThrowIfCancellationRequested();

            // simulação de verificação (chamaria um serviço externo de fraude/pagamento aqui)
            await Task.Delay(200, stoppingToken);

            await repositorio.ConfirmarAsync(pedido.Id);
            _logger.LogInformation("Pedido {PedidoId} confirmado (Cliente {ClienteId}, Valor {Valor:C})",
                pedido.Id, pedido.ClienteId, pedido.ValorTotal);
        }
    }
}

5. Program.cs

C# Program.cs
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddWindowsService(o => o.ServiceName = "ProcessadorDePedidos");
builder.Services.AddSystemd();

builder.Services.Configure<WorkerOptions>(builder.Configuration.GetSection("Worker"));
builder.Services.AddScoped<IPedidoProcessamentoRepository>(_ =>
    new PedidoProcessamentoRepository(builder.Configuration.GetConnectionString("Default")!));
builder.Services.AddHostedService<ProcessadorPedidosWorker>();

var host = builder.Build();
host.Run();

6. O que praticar a partir daqui

Um worker de produção real cresceria em algumas direções diretas: métricas (quantos pedidos processados por execução, tempo médio), alertas quando a fila pendente cresce demais, e idealmente um mecanismo de lock distribuído se múltiplas instâncias do worker rodarem ao mesmo tempo (para não processar o mesmo pedido duas vezes). São todas extensões do que este capítulo já cobriu.

7. Onde isso te leva

Com Worker Services completos, o último módulo deste curso cobre o quarto e último modelo de aplicação C#: interfaces gráficas Desktop, onde a interação acontece através de cliques e data binding, em vez de requisições HTTP ou execução em segundo plano.

📌 Resumo do capítulo

  • Um worker de processamento combina PeriodicTimer, escopo de DI por execução, configuração tipada e logging estruturado — cada peça vista nos capítulos anteriores.
  • O mesmo Program.cs registra o worker tanto para rodar como Serviço do Windows quanto como daemon systemd, sem conflito.
  • Falhas isoladas (um pedido específico, uma execução específica) não deveriam derrubar o worker inteiro.

✏️ Praticando

  1. Implemente o worker completo deste capítulo, incluindo as procedures de apoio.
  2. Crie pedidos pendentes de teste via API (Módulo 4) e confirme que o worker os processa no ciclo seguinte.
  3. Adicione uma métrica simples: um contador de quantos pedidos foram processados desde que o worker iniciou, logado a cada execução.