5 min de lectureIngénierie

Comment raccourcir une URL en C# avec HttpClient et .NET

Raccourcissez une URL en C# avec PostAsJsonAsync, puis configurez IHttpClientFactory, un gestionnaire de résilience, une clé Idempotency-Key et un raccourcissement groupé borné avec Parallel.

Marius Voß
DevRel · edge infra
Comment raccourcir une URL en C# : un HttpClient typé envoie une URL de destination à l'API de raccourcissement et désérialise le lien court de la réponse

Raccourcir une URL en C# tient à un seul POST. Envoyez la destination à l'API du raccourcisseur avec HttpClient, transmettez votre clé d'API comme jeton Bearer et désérialisez short_url depuis la réponse. PostAsJsonAsync s'occupe de la sérialisation : une fois le client enregistré, l'appel tient sur une seule ligne.

La forme du point d'entrée, le modèle d'authentification et les limites de l'offre gratuite supposés ici sont documentés dans la présentation de l'API gratuite de raccourcissement d'URL. Il s'agit de l'article C# d'une série qui couvre également Python, JavaScript, Go et Java.

La méthode la plus rapide : 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

Les noms des propriétés du record correspondent volontairement au snake_case de l'API, ce qui permet de garder l'exemple sans dépendance. Dans un vrai projet, ajoutez [JsonPropertyName("short_url")] et donnez à la propriété un nom C#, ou définissez une fois PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower dans les options.

EnsureSuccessStatusCode est la ligne qui compte. Sans elle, un 401 se termine sans incident, ReadFromJsonAsync renvoie un record contenant des valeurs nulles et l'échec se manifeste trois couches plus loin par une NullReferenceException sur un élément sans rapport.

Enregistrer le client plutôt que de le créer à chaque appel

L'extrait ci-dessus convient à un prototype de console, mais pas à un service. Un HttpClient créé à chaque appel ouvre un nouveau pool de connexions et sa libération laisse le socket dans l'état TIME_WAIT ; faites cela dans une boucle et vous épuiserez les ports. IHttpClientFactory mutualise les gestionnaires et renouvelle correctement le DNS.

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 vient de Microsoft.Extensions.Http.Resilience et vous fournit une nouvelle tentative avec gigue en cas d'échec transitoire, un délai d'expiration total de la requête et un disjoncteur, sans écrire de stratégie. Dans un projet plus ancien, AddTransientHttpErrorPolicy de Polly couvre le même besoin.

Un HttpClient typé en C# envoie une URL de destination avec un jeton Bearer et une Idempotency-Key au point d'entrée des liens du raccourcisseur et désérialise short_url depuis la réponse 201

Le client typé lui-même reste compact :

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;
    }
}

C'est cet en-tête qui permet d'activer le gestionnaire de résilience sans risque. Sans lui, une nouvelle tentative après une réponse perdue crée un second lien pour la même destination ; avec lui, l'API renvoie le lien d'origine. L'analyse approfondie des limites de débit et de l'idempotence explique pourquoi dériver la clé de la destination est préférable à un GUID généré aléatoirement pour les traitements par lot.

Une application console, un service ASP.NET Core et une Azure Function enregistrent tous le même client typé et envoient le même POST au point d'entrée du raccourcisseur d'URL

Vous voulez l'essayer sur un point d'entrée réel ? Créez une clé avec l'offre gratuite, placez-la dans les secrets utilisateur sous Elido:ApiKey et l'enregistrement ci-dessus fonctionnera sans modification.

Lire le corps de l'erreur plutôt que de deviner

EnsureSuccessStatusCode est le bon choix par défaut, mais il élimine la partie la plus utile d'un appel en échec. Le message de l'exception contient le code de statut et rien d'autre : un 422 vous indique que la requête a été refusée sans préciser quel champ a posé problème à l'API.

Pour tout ce que vous devrez déboguer à trois heures du matin, lisez d'abord le corps :

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}"),
    };
}

Deux conséquences en découlent. La ligne de journal nomme le champ en cas d'erreur de validation au lieu de vous laisser reconstituer la requête, et le switch sépare les échecs qui valent la peine d'une nouvelle tentative de ceux qui ne réussiront jamais. Un 401 réessayé trois fois, ce sont trois appels perdus et un échec plus lent.

Le gestionnaire de résilience standard traite déjà les 5xx et les 429 comme transitoires et laisse les 4xx tranquilles : les deux couches sont donc cohérentes. Il réessaie ce qui mérite de l'être, et votre code explique le reste.

Traitement groupé sans dépasser la limite de débit

Task.WhenAll appliqué à une liste de 3 000 URL lance 3 000 requêtes. Parallel.ForEachAsync limite le nombre de requêtes en cours et se lit mieux qu'un sémaphore écrit manuellement :

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
        }
    });

Huit est un point de départ, pas une constante. Surveillez les 429 et l'en-tête Retry-After envoyé par l'API, puis réduisez la valeur plutôt que de l'augmenter. L'indexation par l'URL d'origine permet à une exécution interrompue de vous indiquer exactement quelles lignes refaire.

Où le code doit-il se trouver ?

Un outil console qui raccourcit un CSV, un service hébergé qui crée un lien lorsqu'une commande est expédiée, une Azure Function déclenchée par une file d'attente : le même client typé, trois enregistrements différents. La seule chose qui ne doit pas varier est l'origine de la clé. Secrets utilisateur en développement, variables d'environnement ou coffre en production, IOptions entre les deux, et rien dans appsettings.json qui se retrouverait dans le dépôt.

Une fois les liens créés depuis un service, ce que l'API expose d'autre aux développeurs devient la partie intéressante, et les webhooks pour les événements de lien sont préférables à l'interrogation pour les données de clics et de scans. Si vous préférez ne pas maintenir le client vous-même, la page API et SDK répertorie les clients générés.

Lire la série pilier

Cet article appartient au cluster engineering. Commencez par le guide de l'API gratuite de raccourcissement d'URL pour la forme du point d'entrée et l'authentification, puis consultez les limites de débit et l'idempotence pour le comportement sous charge. La référence en ligne se trouve dans la documentation de l'API.

À lire aussi sur le blog

Questions fréquentes

Comment raccourcir une URL en C# ?

Envoyez l'URL longue à l'API d'un raccourcisseur avec HttpClient, en utilisant PostAsJsonAsync pour sérialiser le corps à votre place, puis désérialisez la réponse dans un petit record et lisez ShortUrl. Une fois l'en-tête Authorization défini sur le client, l'appel lui-même tient sur une seule ligne.

Pourquoi ne dois-je pas utiliser new HttpClient() dans une boucle ?

Chaque instance ouvre son propre pool de connexions et sa libération laisse le socket dans l'état TIME_WAIT, de sorte qu'une boucle peut épuiser les ports disponibles sous charge. Enregistrez le client avec IHttpClientFactory et injectez-le : les gestionnaires sont mutualisés et le DNS est renouvelé correctement.

HttpClient lève-t-il une exception pour une réponse 401 ou 500 ?

Pas par défaut. La tâche se termine correctement et IsSuccessStatusCode vaut false, de sorte qu'un appel en échec ressemble à un appel fonctionnel si vous ne vérifiez pas son résultat. Appelez EnsureSuccessStatusCode pour le transformer en HttpRequestException, ou testez vous-même le statut avant de lire le corps.

Comment ajouter des nouvelles tentatives à HttpClient dans .NET ?

Ajoutez le gestionnaire de résilience standard de Microsoft.Extensions.Http.Resilience à l'enregistrement du client. Il apporte des nouvelles tentatives avec un backoff avec gigue, un délai d'expiration total de la requête et un disjoncteur, sans code par appel. Dans les projets plus anciens, AddTransientHttpErrorPolicy de Polly fait le même travail.

Comment raccourcir de nombreuses URL en une seule fois en C# ?

Utilisez Parallel.ForEachAsync avec MaxDegreeOfParallelism défini sur environ huit : le nombre de requêtes en cours reste ainsi limité quelle que soit la longueur de la liste. Rassemblez les résultats dans un ConcurrentDictionary indexé par l'URL d'origine afin qu'un échec partiel reste visible et que le lot puisse être relancé.

Où la clé d'API doit-elle être stockée dans une application .NET ?

Dans une configuration qui n'est pas versionnée : secrets utilisateur en développement, variables d'environnement ou coffre de clés en production. Liez-la avec IOptions et lisez-la lors de l'enregistrement du client : la rotation de la clé devient ainsi une modification de configuration plutôt qu'une nouvelle compilation.

Essayer Elido

Collez une URL, obtenez un lien court

Sans inscription. Lien actif 30 jours. Inscrivez-vous pour le garder pour toujours.

Gratuit, sans inscription · 2 par jour

Essayer Elido

Raccourcisseur d'URL hébergé en UE : domaines personnalisés, analyses approfondies et API ouverte. Forfait gratuit - sans carte bancaire.

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

Lire la suite