4 хв читанняІнженерія

Як скоротити URL у C# за допомогою HttpClient і .NET

Скоротіть URL у C# за допомогою PostAsJsonAsync, а потім підключіть IHttpClientFactory, обробник стійкості, Idempotency-Key і обмежене масове скорочення через Parallel.

Marius Voß
DevRel · edge infra
Як скоротити URL у C#: типізований HttpClient надсилає URL призначення до API скорочувача, а з відповіді десеріалізує коротке посилання

Скорочення URL у C# - це один POST-запит. Надішліть URL призначення до API скорочувача через HttpClient, передайте ключ API як Bearer-токен і десеріалізуйте short_url із відповіді. PostAsJsonAsync виконує серіалізацію, тому після реєстрації клієнта виклик займає один рядок.

Формат endpoint-а, модель автентифікації та ліміти безкоштовного рівня, припущені тут, описано в огляді безкоштовного API скорочувача URL. Це матеріал про C# у серії, яка також охоплює Python, JavaScript, Go і Java.

Найшвидший спосіб: 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

Назви властивостей record навмисно відповідають snake_case API, завдяки чому приклад не має залежностей. У реальному проєкті додайте [JsonPropertyName("short_url")] і надайте властивості назву в стилі C#, або один раз задайте PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower у параметрах.

EnsureSuccessStatusCode - це важливий рядок. Без нього 401 завершується начебто успішно, ReadFromJsonAsync повертає record із null-значеннями, а помилка проявляється через три рівні як NullReferenceException у чомусь, що не має стосунку до проблеми.

Реєструйте клієнт, а не створюйте його щоразу

Наведений вище фрагмент підходить для експерименту в консольному застосунку, але непридатний для сервісу. Створений для кожного виклику HttpClient щоразу відкриває новий пул з'єднань, а його звільнення залишає сокет у TIME_WAIT; якщо робити це в циклі, порти закінчаться. IHttpClientFactory використовує спільний пул обробників і коректно оновлює 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 походить із Microsoft.Extensions.Http.Resilience і додає повторну спробу з джитером для тимчасових помилок, загальний тайм-аут запиту та автоматичний вимикач без написання політики. У старішому проєкті Polly's AddTransientHttpErrorPolicy забезпечує те саме.

Типізований C# HttpClient надсилає URL призначення із Bearer-токеном і Idempotency-Key до endpoint-а посилань скорочувача та десеріалізує short_url із відповіді 201

Сам типізований клієнт залишається невеликим:

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

Саме цей заголовок робить безпечним увімкнення обробника стійкості. Без нього повторна спроба після втраченої відповіді створює друге посилання для того самого призначення; з ним API повертає оригінальне. У детальному розборі лімітів запитів та ідемпотентності пояснено, чому для пакетної роботи краще виводити ключ із URL призначення, а не використовувати новий GUID.

Консольний застосунок, сервіс ASP.NET Core та Azure Function реєструють той самий типізований клієнт і надсилають ідентичний POST до endpoint-а скорочувача URL

Хочете перевірити це на робочому endpoint-і? Створіть ключ на безкоштовному плані, покладіть його в user secrets як Elido:ApiKey, і наведена вище реєстрація працюватиме без змін.

Читайте тіло помилки, а не вгадуйте

EnsureSuccessStatusCode - правильне значення за замовчуванням, але воно відкидає найкориснішу частину невдалого виклику. Повідомлення винятку містить код статусу і нічого більше, тому 422 повідомляє, що запит відхилено, але не каже, яке поле не сподобалося API.

Для всього, що доведеться налагоджувати о третій ночі, спочатку прочитайте тіло:

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

З цього випливають дві речі. Рядок логу називає поле з помилкою валідації, тож вам не потрібно відновлювати запит, а switch відділяє помилки, для яких варто робити повторну спробу, від тих, які ніколи не завершаться успішно. Повторити 401 тричі означає зробити три марні виклики й отримати повільнішу помилку.

Стандартний обробник стійкості уже вважає 5xx і 429 тимчасовими помилками, а 4xx залишає без змін, тому обидва рівні узгоджені: обробник повторює те, що варто повторювати, а ваш код пояснює решту.

Масове скорочення без перевищення ліміту запитів

Task.WhenAll для списку з 3 000 URL запускає 3 000 запитів. Parallel.ForEachAsync обмежує кількість запитів у роботі й читається краще, ніж 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
        }
    });

Вісім - це початкове значення, а не константа. Стежте за 429 і заголовком Retry-After, який надсилає API, та за потреби зменшуйте число, а не збільшуйте його. Ключі у вигляді початкових URL означають, що після незавершеного запуску ви точно знаєте, які рядки повторити.

Де має зберігатися код

Консольний інструмент, що скорочує CSV, фоновий сервіс, який створює посилання після відправлення замовлення, Azure Function, що реагує на повідомлення в черзі: той самий типізований клієнт, але три різні реєстрації. Єдине, що не повинно змінюватися, - джерело ключа. User secrets під час розробки, змінні середовища або сховище ключів у продакшені, IOptions між ними, і нічого в appsettings.json, що потрапить до репозиторію.

Коли сервіс уже створює посилання, цікавішим стає те, що ще API пропонує розробникам, а вебхуки для подій посилань кращі за опитування для даних про кліки й сканування. Якщо ви не хочете самостійно підтримувати клієнт, на сторінці API та SDK перелічено згенеровані варіанти.

Прочитайте основну серію

Цей матеріал належить до інженерного кластера. Почніть із посібника з безкоштовного API скорочувача URL, щоб розібратися з форматом endpoint-а й автентифікацією, а потім перейдіть до лімітів запитів та ідемпотентності, щоб вивчити поведінку під навантаженням. Актуальна довідка доступна в документації API.

Пов'язані матеріали в блозі

Поширені запитання

Як скоротити URL у C#?

Надішліть довгий URL до API скорочувача через HttpClient, використовуючи PostAsJsonAsync, щоб тіло було серіалізовано автоматично, потім десеріалізуйте відповідь у невеликий record і прочитайте ShortUrl. Якщо заголовок Authorization задано на клієнті, сам виклик займає один рядок.

Чому не слід використовувати new HttpClient() у циклі?

Кожен екземпляр відкриває власний пул з'єднань, а його звільнення залишає сокет у TIME_WAIT, тому під навантаженням цикл може вичерпати доступні порти. Зареєструйте клієнт через IHttpClientFactory та впровадьте його, щоб обробники використовували спільний пул, а DNS коректно оновлювався.

Чи викидає HttpClient виняток для відповіді 401 або 500?

За замовчуванням ні. Завдання завершується успішно, а IsSuccessStatusCode має значення false, тому невдалий виклик виглядає робочим, якщо це не перевірити. Викличте EnsureSuccessStatusCode, щоб перетворити його на HttpRequestException, або перевірте статус самостійно перед читанням тіла.

Як додати повторні спроби до HttpClient у .NET?

Додайте стандартний обробник стійкості з Microsoft.Extensions.Http.Resilience до реєстрації клієнта. Він додає повторні спроби з джитером у backoff, загальний тайм-аут запиту та автоматичний вимикач без коду для кожного виклику. У старіших проєктах Polly's AddTransientHttpErrorPolicy виконує ту саму роль.

Як скоротити багато URL одночасно в C#?

Використайте Parallel.ForEachAsync із MaxDegreeOfParallelism, встановленим приблизно на вісім: це обмежує кількість запитів у роботі незалежно від довжини списку. Збирайте результати в ConcurrentDictionary з ключами у вигляді початкових URL, щоб часткова помилка залишалася видимою, а пакет можна було запустити повторно.

Де зберігати ключ API у .NET-застосунку?

У конфігурації, яку не додають до репозиторію: user secrets під час розробки, змінні середовища або сховище ключів у продакшені. Прив'яжіть його через IOptions і прочитайте під час реєстрації клієнта, щоб ротація ключа вимагала зміни конфігурації, а не повторної збірки.

Спробуйте Elido

Вставте URL - отримайте коротке посилання

Без реєстрації. Посилання живе 30 днів. Зареєструйтесь, щоб зберегти назавжди.

Безкоштовно, без реєстрації · 2 на день

Спробуйте Elido

URL-скорочувач із хостингом у ЄС: власні домени, глибока аналітика, відкритий API. Безкоштовний тариф - без кредитної картки.

Теги
how to shorten a url in c#
c# url shortener api
httpclient postasjsonasync
dotnet shorten url
ihttpclientfactory typed client
bulk shorten urls dotnet

Читати далі