5 Min. LesezeitEngineering

Eine URL in C# mit HttpClient und .NET kürzen

Kürzen Sie eine URL in C# mit PostAsJsonAsync und binden Sie anschließend IHttpClientFactory, einen Resilienz-Handler, einen Idempotency-Key und eine begrenzte Massenkürzung mit Parallel ein.

Marius Voß
DevRel · edge infra
So kürzen Sie eine URL in C#: Ein typisierter HttpClient sendet eine Ziel-URL an eine Shortener-API und deserialisiert den Short-Link aus der Antwort

Eine URL in C# zu kürzen ist ein einziger POST. Senden Sie das Ziel mit HttpClient an die API des URL-Shorteners, übergeben Sie Ihren API-Schlüssel als Bearer-Token und deserialisieren Sie short_url aus der Antwort. PostAsJsonAsync übernimmt die Serialisierung, sodass der Aufruf nach der Registrierung des Clients nur eine Zeile lang ist.

Die hier vorausgesetzte Struktur des Endpunkts, das Auth-Modell und die Limits des kostenlosen Tarifs sind in der Übersicht zur kostenlosen URL-Shortener-API dokumentiert. Dies ist der C#-Beitrag einer Serie, die auch Python, JavaScript, Go und Java behandelt.

Der schnellste Weg: 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

Die Namen der Record-Eigenschaften entsprechen absichtlich dem snake_case der API, sodass das Beispiel ohne zusätzliche Abhängigkeiten auskommt. Fügen Sie in einem echten Projekt [JsonPropertyName("short_url")] hinzu und geben Sie der Eigenschaft einen C#-Namen, oder setzen Sie einmalig PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower in den Optionen.

EnsureSuccessStatusCode ist die entscheidende Zeile. Ohne sie wird ein 401 scheinbar erfolgreich abgeschlossen, ReadFromJsonAsync gibt einen Record mit null-Werten zurück und der Fehler tritt drei Ebenen entfernt als NullReferenceException bei etwas auf, das damit nichts zu tun hat.

Den Client registrieren, statt ihn mit new zu erzeugen

Das obige Snippet ist für einen kurzen Test in einer Console-Anwendung in Ordnung, aber in einem Service falsch. Ein pro Aufruf erzeugter HttpClient öffnet jedes Mal einen neuen Verbindungspool, und beim Freigeben bleibt der Socket in TIME_WAIT. Wenn Sie das in einer Schleife tun, gehen Ihnen die Ports aus. IHttpClientFactory poolt die Handler und rotiert DNS korrekt.

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 stammt aus Microsoft.Extensions.Http.Resilience und liefert bei vorübergehenden Fehlern Retries mit Jitter-Backoff, ein Timeout für die gesamte Anfrage und einen Circuit Breaker, ohne dass Sie eine Policy schreiben müssen. In einem älteren Projekt deckt Pollys AddTransientHttpErrorPolicy dasselbe ab.

Ein typisierter C#-HttpClient sendet eine Ziel-URL mit Bearer-Token und Idempotency-Key an den Links-Endpunkt des Shorteners und deserialisiert short_url aus der 201-Antwort

Der typisierte Client selbst bleibt klein:

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

Dieser Header macht es sicher, den Resilienz-Handler zu aktivieren. Ohne ihn erzeugt ein Retry nach einer verlorenen Antwort einen zweiten Link für dasselbe Ziel. Mit ihm gibt die API den ursprünglichen zurück. Der Deep-Dive zu Rate Limits und Idempotenz erklärt, warum es bei Batch-Aufgaben besser ist, den Schlüssel aus dem Ziel abzuleiten als eine neue GUID zu verwenden.

Eine Console-Anwendung, ein ASP.NET-Core-Service und eine Azure Function registrieren denselben typisierten Client und senden denselben POST an den URL-Shortener-Endpunkt

Möchten Sie den Client gegen einen Live-Endpunkt ausprobieren? Erstellen Sie einen Schlüssel im kostenlosen Tarif, legen Sie ihn als Elido:ApiKey in den User Secrets ab, und die obige Registrierung funktioniert unverändert.

Den Fehler-Body lesen, statt zu raten

EnsureSuccessStatusCode ist der richtige Standard, verwirft aber den nützlichsten Teil eines fehlgeschlagenen Aufrufs. Die Fehlermeldung der Ausnahme enthält den Statuscode und sonst nichts. Ein 422 sagt Ihnen daher, dass die Anfrage abgelehnt wurde, aber nicht, welches Feld die API beanstandet hat.

Bei allem, was Sie um drei Uhr morgens debuggen müssen, sollten Sie zuerst den Body lesen:

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

Daraus ergeben sich zwei Dinge. Die Log-Zeile nennt bei einem Validierungsfehler das Feld, statt Sie die Anfrage rekonstruieren zu lassen, und der Switch trennt Fehler, bei denen sich ein Retry lohnt, von solchen, die nie erfolgreich sein werden. Ein dreimal wiederholter 401-Fehler bedeutet drei verschwendete Aufrufe und einen langsameren Fehlschlag.

Der standardmäßige Resilienz-Handler behandelt 5xx und 429 bereits als vorübergehend und lässt 4xx in Ruhe. Die beiden Ebenen stimmen also überein: Er wiederholt, was sich zu wiederholen lohnt, und Ihr Code erklärt, was übrig bleibt.

Massenkürzung, ohne das Rate Limit zu sprengen

Task.WhenAll über einer Liste mit 3.000 URLs startet 3.000 Anfragen. Parallel.ForEachAsync begrenzt die Zahl der gleichzeitig laufenden Anfragen und ist besser lesbar als ein selbst geschriebener Semaphore:

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

Acht ist ein Ausgangspunkt, keine Konstante. Beobachten Sie 429-Fehler und den Retry-After-Header, den die API sendet, und passen Sie den Wert nach unten statt nach oben an. Durch die ursprüngliche URL als Schlüssel zeigt Ihnen ein halb abgeschlossener Lauf genau, welche Zeilen Sie erneut verarbeiten müssen.

Wo der Code hingehört

Ein Console-Tool, das eine CSV kürzt, ein gehosteter Service, der beim Versand einer Bestellung einen Link erstellt, und eine Azure Function als Queue-Trigger: derselbe typisierte Client, drei verschiedene Registrierungen. Nur die Quelle des Schlüssels darf sich nicht ändern. User Secrets in der Entwicklung, Umgebungsvariablen oder ein Vault in der Produktion, dazwischen IOptions, und nichts in appsettings.json, das im Repository landet.

Sobald Links aus einem Service heraus erstellt werden, wird was die API Entwicklern sonst noch bietet interessant, und Webhooks für Link-Ereignisse sind für Klick- und Scan-Daten besser als Polling. Wenn Sie den Client lieber gar nicht selbst warten möchten, listet die Seite API und SDKs die generierten Varianten auf.

Die Cornerstone-Serie lesen

Dieser Beitrag gehört zum Engineering-Cluster. Beginnen Sie mit dem Leitfaden zur kostenlosen URL-Shortener-API für die Struktur des Endpunkts und die Authentifizierung, und lesen Sie anschließend Rate Limits und Idempotenz zum Verhalten unter Last. Die aktuelle Referenz sind die API-Dokumente.

Verwandt im Blog

Häufig gestellte Fragen

Wie kürze ich eine URL in C#?

Senden Sie die lange URL mit HttpClient per POST an die API eines URL-Shorteners und verwenden Sie PostAsJsonAsync, damit der Body für Sie serialisiert wird. Deserialisieren Sie anschließend die Antwort in einen kleinen Record und lesen Sie ShortUrl aus. Wenn der Authorization-Header am Client gesetzt ist, besteht der Aufruf selbst aus einer einzigen Zeile.

Warum sollte ich new HttpClient() nicht in einer Schleife verwenden?

Jede Instanz öffnet ihren eigenen Verbindungspool, und beim Freigeben bleibt der Socket in TIME_WAIT. Unter Last kann eine Schleife deshalb die verfügbaren Ports aufbrauchen. Registrieren Sie den Client mit IHttpClientFactory und injizieren Sie ihn. Dadurch werden die Handler gepoolt und DNS korrekt rotiert.

Löst HttpClient bei einer 401- oder 500-Antwort eine Ausnahme aus?

Nicht standardmäßig. Der Task wird erfolgreich abgeschlossen und IsSuccessStatusCode ist false. Daher sieht ein fehlgeschlagener Aufruf wie ein funktionierender aus, wenn Sie nicht prüfen. Rufen Sie EnsureSuccessStatusCode auf, um daraus eine HttpRequestException zu machen, oder prüfen Sie den Status selbst, bevor Sie den Body lesen.

Wie füge ich HttpClient in .NET Retries hinzu?

Fügen Sie den standardmäßigen Resilienz-Handler aus Microsoft.Extensions.Http.Resilience zur Client-Registrierung hinzu. Er bringt Retries mit Jitter-Backoff, ein Timeout für die gesamte Anfrage und einen Circuit Breaker ohne Code pro Aufruf. In älteren Projekten erledigt Pollys AddTransientHttpErrorPolicy dieselbe Aufgabe.

Wie kürze ich viele URLs auf einmal in C#?

Verwenden Sie Parallel.ForEachAsync mit einem auf ungefähr acht gesetzten MaxDegreeOfParallelism. Dadurch wird unabhängig von der Listenlänge begrenzt, wie viele Anfragen gleichzeitig laufen. Sammeln Sie die Ergebnisse in einem ConcurrentDictionary, dessen Schlüssel die ursprünglichen URLs sind, damit ein Teilfehler sichtbar bleibt und der Batch erneut ausgeführt werden kann.

Wo sollte der API-Schlüssel in einer .NET-App liegen?

In einer Konfiguration, die nicht eingecheckt wird: User Secrets in der Entwicklung, Umgebungsvariablen oder ein Key Vault in der Produktion. Binden Sie ihn mit IOptions und lesen Sie ihn bei der Client-Registrierung aus. So bedeutet ein rotierter Schlüssel eine Konfigurationsänderung statt eines Rebuilds.

Elido testen

URL einfügen, kurzer Link in Sekunden

Kein Konto nötig. Link bleibt 30 Tage aktiv. Konto erstellen, um ihn dauerhaft zu behalten.

Kostenlos, keine Anmeldung erforderlich · 2 pro Tag

Elido testen

URL-Shortener mit EU-Hosting: eigene Domains, tiefe Analytik und eine offene API. Kostenloser Tarif - keine Kreditkarte nötig.

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

Weiterlesen