A maior e melhor Black Friday que já fizemos! Descontos a partir de 50% em até 12x sem juros - Garanta seu Desconto

Webhooks no .NET: o que são e como implementar

Webhooks no .NET: o que são e como implementar

Webhooks no .NET resolvem um problema comum: descobrir, em tempo real, que algo aconteceu em um sistema que você não controla. Neste guia, você entende o conceito, compara a abordagem com o polling e implementa um emissor e um receptor completos com ASP.NET Core, Minimal APIs e Blazor no .NET 10.

O que são webhooks no .NET

Um webhook é uma chamada HTTP que um sistema externo faz para uma URL sua sempre que um evento acontece. Em outras palavras, em vez de você perguntar se há novidades, o outro sistema avisa você.

Na prática, os webhooks no .NET são apenas endpoints HTTP que recebem dados enviados por terceiros. Por isso, o conceito parece simples demais à primeira vista, e de fato é.

Pense em uma loja virtual que usa um serviço terceirizado de pagamentos. Quando o cliente conclui a compra, a transação acontece no serviço de pagamento, e não na sua aplicação.

Então, como a sua aplicação descobre que a compra aconteceu? O serviço de pagamento chama uma URL que você cadastrou e envia os dados da transação no corpo da requisição.

Esse é exatamente o cenário que motivou o exemplo deste artigo. Além disso, ele mostra a característica mais importante desse modelo: os dois sistemas não se conhecem diretamente.

O sistema externo não tem acesso ao seu código, ao seu banco de dados nem à sua infraestrutura. Ele conhece apenas uma URL para chamar e o formato dos dados que deve enviar.

Por outro lado, a sua aplicação não controla o formato desses dados. Ela recebe o que chega e decide como processar cada informação.

Polling ou webhook: qual a diferença

Existem duas formas principais de obter dados de um sistema externo. A primeira é o polling, e a segunda é o webhook.

No polling, a sua aplicação consulta o sistema externo repetidamente. Ela pergunta se existe algo novo, recebe um não, espera um pouco e pergunta de novo.

Esse modelo funciona em alguns cenários, porém traz dois problemas sérios. Primeiro, você sobrecarrega o sistema externo com chamadas que, na maioria das vezes, não retornam nada.

Segundo, você precisa pedir todos os dados novos de uma vez, e não apenas o evento que interessa. Assim, cada consulta traz uma lista de transações para filtrar.

Muitos provedores percebem esse abuso e impõem limites. Por exemplo, um serviço pode permitir apenas uma consulta a cada 15 minutos.

Esse limite parece razoável, mas cria um atraso real. Se o cliente comprou no minuto dois, ele espera mais 13 minutos até a sua aplicação perceber a compra.

Os webhooks no .NET invertem a direção da comunicação. Em vez de a sua aplicação buscar os dados, o sistema externo empurra os dados assim que o evento acontece.

Comparação entre polling e webhooks no .NET: no polling a aplicação pergunta repetidamente ao sistema externo, no webhook o sistema externo envia um POST quando o evento acontece
No polling, a iniciativa é de quem consome os dados. No webhook, a iniciativa é de quem gera o evento.

Com isso, o atraso praticamente desaparece. Além disso, o sistema externo envia apenas os dados do evento que ocorreu, sem listas para filtrar.

Como os webhooks no .NET funcionam na prática

O exemplo deste artigo usa duas aplicações separadas, em soluções diferentes. Essa separação simula o fato de que, no mundo real, você raramente controla os dois lados.

A primeira aplicação é o site externo, que representa o serviço de pagamentos. Ela tem um formulário de compra com nome do item, preço e quantidade.

A segunda aplicação é o seu site, que recebe os eventos. Ela tem uma página que lista as compras assim que chegam.

As duas são Blazor Web Apps com renderização interativa no servidor. No entanto, os webhooks no .NET funcionam do mesmo jeito em Razor Pages, MVC ou uma API pura.

O fluxo completo segue quatro etapas. Primeiro, o usuário envia o formulário no site externo.

Em seguida, o site externo faz um POST com os dados da compra para a URL do webhook. Depois, o seu site recebe a chamada, guarda a compra e dispara um evento interno.

Por fim, a página de compras escuta esse evento e atualiza a interface na hora, sem recarregar nada.

Fluxo dos webhooks no .NET: formulário no site externo, POST HTTPS para /webhooks/purchases, endpoint MapPost, PurchaseStore com ConcurrentQueue, evento PurchaseAdded e página Blazor atualizada
O caminho de uma compra, do formulário no site externo até a tabela na página do seu site.

Em um serviço real, você não altera o código do sistema externo. Normalmente, o provedor oferece um formulário de configuração de webhooks.

Nesse formulário, você escolhe quais eventos quer capturar e informa a URL que deve ser chamada. O provedor então guarda essa configuração e passa a chamar a sua URL quando o evento ocorre.

Também é comum separar eventos diferentes em webhooks diferentes. Por exemplo, uma compra avulsa e uma assinatura podem exigir processamentos distintos, então cada uma aponta para um endpoint próprio.

Enviando webhooks no .NET com IHttpClientFactory

O lado emissor é o mais simples. Para enviar webhooks no .NET, ele precisa apenas fazer uma requisição HTTP com os dados do evento.

Antes disso, vale olhar o modelo do site externo. Ele representa o formulário que o usuário preenche, por isso carrega validações.

using System.ComponentModel.DataAnnotations;

namespace ExternalSite.Models;

public class PurchaseModel
{
    [Required]
    [MinLength(3)]
    public string ItemName { get; set; } = string.Empty;

    // Para decimal, o Range exige o tipo e os limites como string
    [Range(typeof(decimal), "0.01", "1000")]
    public decimal ItemPrice { get; set; }

    [Range(1, 10)]
    public int Quantity { get; set; }
}

Repare no atributo Range da propriedade ItemPrice. Os construtores simples aceitam apenas int e double.

Para outros tipos, como decimal, você informa o tipo com typeof e passa os limites como texto. Então o atributo converte esses valores e aplica a validação.

Registrando o IHttpClientFactory

O primeiro passo é registrar o IHttpClientFactory no Program.cs do site externo. Para isso, basta uma linha.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// Registra o IHttpClientFactory no container de injeção de dependência
builder.Services.AddHttpClient();

Use sempre a factory em vez de criar instâncias de HttpClient manualmente. Segundo a documentação oficial do IHttpClientFactory, ela reaproveita os handlers e evita problemas como o esgotamento de sockets.

Disparando o webhook no envio do formulário

Em seguida, a página de compra injeta a factory e chama o webhook dentro do método de envio. O restante da página continua sendo um EditForm comum.

@page "/purchase-items"
@rendermode InteractiveServer
@inject IHttpClientFactory HttpClientFactory

@if (!string.IsNullOrWhiteSpace(submissionText))
{
    <p class="alert alert-success">@submissionText</p>
}

<EditForm Model="model" OnValidSubmit="SubmitPurchase">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <InputText @bind-Value="model.ItemName" />
    <InputNumber @bind-Value="model.ItemPrice" />
    <InputNumber @bind-Value="model.Quantity" />
    <button type="submit">Submit</button>
</EditForm>

@code {
    private PurchaseModel model = new();
    private string? submissionText;

    private async Task SubmitPurchase()
    {
        var client = HttpClientFactory.CreateClient();

        // Envia o evento para a URL cadastrada pelo receptor
        await client.PostAsJsonAsync(
            "https://localhost:7170/webhooks/purchases",
            model);

        model = new();
        submissionText = "The purchase has been successfully submitted.";
    }
}

O método PostAsJsonAsync serializa o modelo em JSON e envia o resultado no corpo da requisição. Ou seja, todo o trabalho do emissor cabe em duas linhas.

Note que esse emissor de webhooks no .NET ainda não trata falhas nem reenvios. Ele assume que a chamada sempre funciona, o que é aceitável para entender o conceito, mas não para produção.

Recebendo webhooks no .NET com Minimal APIs

O lado receptor tem mais responsabilidades. Para receber webhooks no .NET, ele precisa expor um endpoint, guardar os dados e avisar o restante da aplicação.

Uma dúvida comum aparece aqui: como criar uma API dentro de um projeto Blazor? A resposta está na base de tudo.

Blazor, Razor Pages e MVC rodam sobre o mesmo host do ASP.NET Core. Portanto, você pode mapear endpoints de API no mesmo projeto, sem criar outra aplicação.

Se você ainda não conhece esse modelo, vale ler o artigo sobre Minimal APIs no ASP.NET antes de seguir.

Criando o PurchaseStore com ConcurrentQueue

Antes do endpoint, o receptor precisa de um lugar para guardar as compras. Em um sistema real, esse lugar seria um banco de dados.

Para manter o exemplo enxuto, o artigo usa um armazenamento em memória. Assim, o foco permanece no comportamento dos webhooks no .NET.

using System.Collections.Concurrent;
using MySite.Models;

namespace MySite.Services;

public class PurchaseStore
{
    // Fila thread-safe que preserva a ordem de chegada
    public ConcurrentQueue<PurchaseModel> Purchases { get; } = new();

    // Avisa quem estiver ouvindo que uma compra chegou
    public event Action? PurchaseAdded;

    public void Add(PurchaseModel purchase)
    {
        Purchases.Enqueue(purchase);
        PurchaseAdded?.Invoke();
    }
}

A escolha do ConcurrentQueue não é acidental. Ele é uma coleção FIFO segura para acesso concorrente, então duas chamadas quase simultâneas entram na ordem correta.

Uma List comum não oferece essa garantia. Afinal, várias requisições podem chegar ao mesmo tempo em threads diferentes.

O evento PurchaseAdded também merece atenção. O operador ?. garante que o evento só é disparado quando existe ao menos um assinante.

Esse evento é interno à sua aplicação. Ele permite que zero, uma ou várias partes do sistema reajam a uma nova compra imediatamente.

Depois, registre o serviço como singleton no Program.cs. Dessa forma, todas as requisições e páginas compartilham a mesma instância.

builder.Services.AddSingleton<PurchaseStore>();

Mapeando o endpoint com MapPost

Agora sim o endpoint entra em cena. Ele fica no Program.cs do seu site, logo depois de MapStaticAssets.

app.MapStaticAssets();

app.MapPost("/webhooks/purchases", (PurchaseModel purchase, PurchaseStore store) =>
{
    store.Add(purchase);
    return Results.Ok();
});

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

O handler recebe dois parâmetros, e o ASP.NET Core descobre sozinho a origem de cada um. Como o PurchaseStore está registrado no container, ele vem da injeção de dependência.

Já o PurchaseModel não está registrado, então o framework o lê do corpo da requisição em JSON. A documentação de parameter binding descreve essa inferência em detalhes.

Por fim, Results.Ok() devolve um status 200. Esse retorno é importante nos webhooks no .NET, porque informa ao emissor que a entrega funcionou.

Por que não compartilhar o modelo entre os projetos

O receptor também tem um PurchaseModel, com o mesmo formato do emissor. Ainda assim, ele não vem de um projeto compartilhado.

namespace MySite.Models;

// Sem validações: o receptor aceita o que o provedor enviar
public class PurchaseModel
{
    public string ItemName { get; set; } = string.Empty;
    public decimal ItemPrice { get; set; }
    public int Quantity { get; set; }
}

Essa decisão é intencional em webhooks no .NET. O modelo do emissor existe para validar um formulário, enquanto o modelo do receptor existe para ler dados externos.

Se o provedor mudar levemente o formato, validações rígidas no receptor rejeitariam a entrega. Por isso, o receptor aceita o que chega e valida os dados antes de gravar qualquer coisa.

Além disso, o receptor não precisa capturar todos os campos. Se você só precisa do nome e do preço, basta remover Quantity do modelo, e o binding ignora o restante.

Atualizando a interface Blazor em tempo real

A última peça é a página que mostra as compras. Ela precisa reagir ao evento do PurchaseStore sem que o usuário recarregue a tela.

@page "/purchases"
@rendermode InteractiveServer
@implements IDisposable
@inject PurchaseStore Store

<h1>Purchases</h1>

@if (Store.Purchases.IsEmpty)
{
    <p>No purchases yet.</p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Item Name</th>
                <th>Item Price</th>
                <th>Quantity</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var purchase in Store.Purchases)
            {
                <tr>
                    <td>@purchase.ItemName</td>
                    <td>@purchase.ItemPrice.ToString("C")</td>
                    <td>@purchase.Quantity</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    protected override void OnInitialized()
    {
        // Assina o evento quando a página é criada
        Store.PurchaseAdded += OnPurchaseAdded;
    }

    private void OnPurchaseAdded()
    {
        // O evento chega de fora do contexto de renderização do Blazor
        _ = InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        // Remove a assinatura quando a página é descartada
        Store.PurchaseAdded -= OnPurchaseAdded;
    }
}

Três detalhes desse componente fazem toda a diferença. Primeiro, a página assina o evento em OnInitialized.

Em seguida, entra o InvokeAsync. Nos webhooks no .NET, a chamada chega em uma requisição HTTP, fora do circuito da página e potencialmente em outra thread.

Acontece que o Blazor só permite chamar StateHasChanged no seu próprio contexto de sincronização. Portanto, InvokeAsync agenda a atualização no lugar certo, como explica a documentação de renderização de componentes.

Por último, a página implementa IDisposable. Como o PurchaseStore é um singleton, ele vive mais do que qualquer página.

Sem o Dispose, cada página aberta deixaria uma assinatura presa na memória. Então, sempre que você assinar um evento com +=, remova a assinatura com -=.

Com os webhooks no .NET rodando, o resultado é imediato. Ao enviar o formulário no site externo, a compra aparece na tabela do seu site no mesmo instante.

Se você abrir a página de compras em duas janelas, as duas atualizam juntas. E o emissor também poderia chamar várias URLs, de vários sistemas, para o mesmo evento.

Segurança e resiliência em webhooks no .NET

O exemplo deste artigo deixa o endpoint totalmente aberto, de propósito. No entanto, webhooks no .NET em produção exigem mais cuidado.

Comece pelo transporte. A URL do webhook deve usar HTTPS, porque assim o corpo da requisição trafega criptografado.

Quem estiver no meio do caminho consegue ver o endereço chamado, mas não lê os dados enviados. Por isso, não há motivo para aceitar HTTP puro.

O ponto fraco está no receptor. Se alguém descobrir a sua URL, poderá enviar requisições falsas e injetar dados no seu sistema.

Por isso, provedores sérios assinam cada entrega com um segredo compartilhado. O GitHub, por exemplo, documenta como validar entregas de webhook com um segredo, e o receptor confere a assinatura antes de processar qualquer coisa.

Depois vem a resiliência, que tem dois lados. De um lado, o que acontece se o seu site estiver fora do ar quando o evento ocorrer?

Muitos provedores tentam reenviar a entrega após uma falha. Consequentemente, o mesmo evento pode chegar mais de uma vez.

Do outro lado, o receptor de webhooks no .NET precisa lidar com essas duplicatas. A prática comum é processar eventos de forma idempotente, conferindo o identificador do evento antes de agir.

Também vale responder rápido. O endpoint deve validar, confirmar o recebimento e deixar o processamento pesado para depois.

Quando usar webhooks no .NET

Use webhooks no .NET quando um sistema externo produz eventos que a sua aplicação precisa conhecer rapidamente. Pagamentos, assinaturas, matrículas e mudanças em repositórios são exemplos clássicos.

Plataformas de cursos, gateways de pagamento e ferramentas de automação como o Zapier trabalham dessa forma. Elas avisam o seu sistema, e ele reage.

Por outro lado, o polling ainda faz sentido em alguns casos. Se o provedor não oferece webhooks, ou se o atraso não importa, consultar periodicamente pode ser mais simples.

Também considere que a comunicação de um webhook vai em uma única direção. Para integrações que exigem conversa nos dois sentidos, você ainda precisará chamar a API do provedor.

Conclusão

Webhooks no .NET são, essencialmente, um endpoint que recebe um POST quando algo acontece em outro sistema. Essa simplicidade é justamente o que os torna tão úteis.

No emissor, basta o IHttpClientFactory e uma chamada a PostAsJsonAsync. No receptor, um MapPost, um armazenamento seguro para concorrência e um evento interno resolvem o fluxo.

Na interface, o Blazor atualiza a página com InvokeAsync(StateHasChanged), desde que você remova a assinatura no Dispose. Dessa forma, o usuário vê a compra no instante em que ela acontece.

O próximo passo é proteger o endpoint, validar assinaturas e tratar reenvios. Com essas camadas, os webhooks no .NET deixam de ser uma demonstração e viram uma integração confiável.

FAQ

Qual a diferença entre webhook e API?

Em uma API, a sua aplicação faz a chamada e busca os dados. No webhook, o sistema externo chama a sua aplicação e entrega os dados quando um evento acontece.

Preciso de um projeto de API separado para receber webhooks?

Não. Como Blazor, Razor Pages e MVC rodam sobre o ASP.NET Core, você pode mapear um MapPost no mesmo projeto.

Por que usar ConcurrentQueue em vez de List?

Porque várias requisições de webhook podem chegar ao mesmo tempo. A classe ConcurrentQueue é segura para acesso concorrente e preserva a ordem de chegada.

Por que chamar InvokeAsync antes de StateHasChanged?

Esse evento é disparado fora do contexto de sincronização do Blazor. Por isso, o InvokeAsync leva a atualização para o contexto correto e evita uma exceção.

Webhook com HTTPS já é seguro?

O HTTPS protege os dados durante o transporte, mas não impede que terceiros chamem a sua URL. Para isso, valide a assinatura de cada entrega com um segredo compartilhado.