5 min de leituraEngenharia

Como encurtar uma URL em C# com HttpClient e .NET

Encurte uma URL em C# com PostAsJsonAsync e depois configure IHttpClientFactory, um handler de resiliência, uma Idempotency-Key e o encurtamento em massa limitado com Parallel.

Marius Voß
DevRel · edge infra
Como encurtar uma URL em C#: um HttpClient tipado enviando uma URL de destino para uma API de encurtador e desserializando o link curto da resposta

Encurtar uma URL em C# exige um único POST. Envie o destino para a API do encurtador com HttpClient, passe sua chave de API como um token Bearer e desserialize short_url da resposta. PostAsJsonAsync cuida da serialização, então a chamada ocupa uma linha depois que o cliente é registrado.

O formato do endpoint, o modelo de autenticação e os limites do plano gratuito considerados aqui estão documentados na visão geral da API gratuita de encurtador de URL. Esta é a versão para C# de uma série que também aborda Python, JavaScript, Go e Java.

A maneira mais rápida: PostAsJsonAsync

using System.Net.Http.Json;

record LinkRequest(string destination_url);
record LinkResponse(string id, string short_url);

var http = new HttpClient { BaseAddress = new Uri("https://api.elido.app") };
http.DefaultRequestHeaders.Authorization =
    new("Bearer", Environment.GetEnvironmentVariable("ELIDO_API_KEY"));
http.Timeout = TimeSpan.FromSeconds(10);

var response = await http.PostAsJsonAsync("/v1/links",
    new LinkRequest("https://example.com/spring-sale?utm_source=newsletter"));

response.EnsureSuccessStatusCode();

var link = await response.Content.ReadFromJsonAsync<LinkResponse>();
Console.WriteLine(link!.short_url); // https://s.elido.me/ab12cd

Os nomes das propriedades do record correspondem ao snake_case da API de propósito, o que mantém o exemplo sem dependências. Em um projeto real, adicione [JsonPropertyName("short_url")] e dê à propriedade um nome em C#, ou defina PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower nas opções uma única vez.

EnsureSuccessStatusCode é a linha que importa. Sem ela, um 401 é concluído tranquilamente, ReadFromJsonAsync retorna um record com valores nulos e a falha aparece três camadas depois como uma NullReferenceException em algo sem relação.

Registre o cliente em vez de criar um novo

O trecho acima serve para um protótipo de console, mas está errado em um serviço. Um HttpClient criado a cada chamada abre um pool de conexões novo toda vez, e descartá-lo deixa o socket em TIME_WAIT; faça isso em um loop e você ficará sem portas. IHttpClientFactory agrupa os handlers e rotaciona o DNS corretamente.

builder.Services.AddHttpClient<ElidoClient>(client =>
    {
        client.BaseAddress = new Uri("https://api.elido.app");
        client.DefaultRequestHeaders.Authorization =
            new("Bearer", builder.Configuration["Elido:ApiKey"]);
        client.Timeout = TimeSpan.FromSeconds(10);
    })
    .AddStandardResilienceHandler();   // retries, timeout, circuit breaker

AddStandardResilienceHandler vem de Microsoft.Extensions.Http.Resilience e oferece novas tentativas com jitter em falhas transitórias, um timeout total de requisição e um circuit breaker sem que você precise escrever uma política. Em um projeto mais antigo, AddTransientHttpErrorPolicy do Polly cobre o mesmo cenário.

Um HttpClient tipado em C# enviando uma URL de destino com um token Bearer e uma Idempotency-Key para o endpoint de links do encurtador e desserializando o short_url da resposta 201

O cliente tipado em si continua pequeno:

public sealed class ElidoClient(HttpClient http)
{
    public async Task<string> ShortenAsync(string destination, CancellationToken ct = default)
    {
        var request = new HttpRequestMessage(HttpMethod.Post, "/v1/links")
        {
            Content = JsonContent.Create(new LinkRequest(destination)),
        };

        // stable across retries and across re-runs of the same batch
        request.Headers.Add("Idempotency-Key",
            Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(destination))));

        using var response = await http.SendAsync(request, ct);
        response.EnsureSuccessStatusCode();

        var link = await response.Content.ReadFromJsonAsync<LinkResponse>(ct);
        return link!.short_url;
    }
}

Esse cabeçalho é o motivo pelo qual é seguro ativar o handler de resiliência. Sem ele, uma nova tentativa após a perda da resposta cria um segundo link para o mesmo destino; com ele, a API devolve o original. O guia detalhado sobre limites de taxa e idempotência explica por que derivar a chave do destino é melhor do que usar um GUID novo em operações em massa.

Um app de console, um serviço ASP.NET Core e uma Azure Function registrando o mesmo cliente tipado e enviando o mesmo POST para o endpoint do encurtador de URL

Quer testar com um endpoint ativo? Crie uma chave no plano gratuito, coloque-a nos user secrets como Elido:ApiKey e o registro acima funcionará sem alterações.

Leia o corpo do erro em vez de adivinhar

EnsureSuccessStatusCode é o padrão correto, mas descarta a parte mais útil de uma chamada com falha. A mensagem da exceção traz o código de status e nada mais, então um 422 informa que a requisição foi rejeitada sem dizer de qual campo a API reclamou.

Para qualquer coisa que você precise depurar às três da manhã, leia primeiro o corpo:

using var response = await http.SendAsync(request, ct);

if (!response.IsSuccessStatusCode)
{
    var problem = await response.Content.ReadAsStringAsync(ct);
    logger.LogWarning("shorten failed {Status}: {Body}", (int)response.StatusCode, problem);

    throw response.StatusCode switch
    {
        HttpStatusCode.Unauthorized      => new InvalidOperationException("API key rejected"),
        HttpStatusCode.TooManyRequests   => new HttpRequestException("rate limited"),
        _                                => new HttpRequestException($"shorten failed: {problem}"),
    };
}

Duas coisas resultam disso. A linha de log identifica o campo em um erro de validação, em vez de deixar você reconstruir a requisição, e o switch separa as falhas que vale a pena tentar novamente daquelas que nunca terão sucesso. Repetir uma tentativa com erro 401 três vezes são três chamadas desperdiçadas e uma falha mais lenta.

O handler de resiliência padrão já trata 5xx e 429 como transitórios e deixa os 4xx de lado, então as duas camadas estão de acordo: ele tenta novamente o que vale a pena e seu código explica o que restou.

Operações em massa sem estourar o limite de taxa

Task.WhenAll sobre uma lista de 3.000 URLs inicia 3.000 requisições. Parallel.ForEachAsync limita a quantidade em andamento e é mais legível do que um semáforo feito à mão:

var results = new ConcurrentDictionary<string, string>();

await Parallel.ForEachAsync(
    urls,
    new ParallelOptions { MaxDegreeOfParallelism = 8 },
    async (url, ct) =>
    {
        try
        {
            results[url] = await client.ShortenAsync(url, ct);
        }
        catch (Exception ex)
        {
            results[url] = $"ERROR: {ex.Message}"; // one bad URL must not sink the batch
        }
    });

Oito é um ponto de partida, não uma constante. Observe os 429 e o cabeçalho Retry-After enviado pela API e reduza o valor em vez de aumentá-lo. Usar a URL original como chave significa que uma execução incompleta informa exatamente quais linhas você precisa refazer.

Onde o código deve ficar

Uma ferramenta de console que encurta um CSV, um serviço hospedado que cria um link quando um pedido é enviado, uma Azure Function acionada por uma fila: o mesmo cliente tipado, três registros diferentes. A única coisa que não pode variar é a origem da chave. User secrets no desenvolvimento, variáveis de ambiente ou um cofre em produção, IOptions entre os dois e absolutamente nada em appsettings.json que vá parar no repositório.

Quando os links passam a ser criados por um serviço, o que mais a API oferece aos desenvolvedores se torna a parte interessante, e webhooks para eventos de links são melhores do que polling para dados de cliques e scans. Se você preferir não manter o cliente, a página de API e SDKs lista os clientes gerados.

Leia a série principal

Este artigo faz parte do cluster de engenharia. Comece pelo guia da API gratuita de encurtador de URL para entender o formato do endpoint e a autenticação, depois consulte limites de taxa e idempotência para o comportamento sob carga. A referência atualizada está na documentação da API.

Relacionado no blog

Perguntas frequentes

Como encurto uma URL em C#?

Envie a URL longa para a API de um encurtador com HttpClient, usando PostAsJsonAsync para que o corpo seja serializado por você, depois desserialize a resposta em um record pequeno e leia ShortUrl. Com o cabeçalho Authorization configurado no cliente, a chamada em si ocupa uma única linha.

Por que não devo usar new HttpClient() em um loop?

Cada instância abre seu próprio pool de conexões, e descartá-la deixa o socket em TIME_WAIT, então um loop pode esgotar as portas disponíveis sob carga. Registre o cliente com IHttpClientFactory e injete-o: assim os handlers são agrupados e o DNS é rotacionado corretamente.

O HttpClient lança uma exceção para uma resposta 401 ou 500?

Não por padrão. A tarefa é concluída com sucesso e IsSuccessStatusCode é false, então uma chamada com falha parece funcionar se você não verificar. Chame EnsureSuccessStatusCode para transformá-la em uma HttpRequestException ou teste o status por conta própria antes de ler o corpo.

Como adiciono novas tentativas ao HttpClient no .NET?

Adicione o handler de resiliência padrão de Microsoft.Extensions.Http.Resilience ao registro do cliente. Ele traz novas tentativas com backoff com jitter, um timeout total de requisição e um circuit breaker sem código por chamada. Em projetos mais antigos, AddTransientHttpErrorPolicy do Polly faz o mesmo trabalho.

Como encurto muitas URLs de uma vez em C#?

Use Parallel.ForEachAsync com MaxDegreeOfParallelism definido como aproximadamente oito; isso limita quantas requisições ficam em andamento, independentemente do tamanho da lista. Colete os resultados em um ConcurrentDictionary indexado pela URL original para que uma falha parcial continue visível e o lote possa ser executado novamente.

Onde a chave de API deve ficar em um app .NET?

Em uma configuração que não seja versionada: user secrets no desenvolvimento, variáveis de ambiente ou um cofre de chaves em produção. Vincule-a com IOptions e leia-a durante o registro do cliente, para que a rotação da chave exija uma alteração de configuração, não uma nova compilação.

Experimente Elido

Cole uma URL, obtenha um link curto

Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.

Grátis, sem necessidade de registo · 2 por dia

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito - sem cartão de crédito.

Tags
how to shorten a url in c#
c# url shortener api
httpclient postasjsonasync
dotnet shorten url
ihttpclientfactory typed client
bulk shorten urls dotnet

Continuar lendo