Сокращение 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 с пустыми значениями, а ошибка проявится тремя уровнями ниже как NullReferenceException в несвязанном месте.
Зарегистрируйте клиент вместо создания через new
Приведенный выше фрагмент подходит для небольшого консольного эксперимента, но неверен для сервиса. Созданный для каждого вызова 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 и добавляет повторы с джиттером при временных сбоях, общий тайм-аут запроса и автоматический выключатель без написания политики. В старом проекте ту же задачу решает AddTransientHttpErrorPolicy из Polly.
Сам типизированный клиент остается небольшим:
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.
Хотите проверить его на работающем endpoint? Создайте ключ на бесплатном тарифе, сохраните его в секретах пользователя как 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, запускаемая сообщением из очереди: один типизированный клиент, три разные регистрации. Единственное, что не должно меняться, - источник ключа. Секреты пользователя при разработке, переменные окружения или хранилище ключей в продакшене, IOptions между ними, и ничего в appsettings.json, что попадет в репозиторий.
Когда ссылки начинают создаваться из сервиса, интереснее становится что ещё API предоставляет разработчикам, а webhook для событий ссылок лучше периодического опроса для данных о кликах и сканированиях. Если вы предпочитаете вообще не поддерживать клиент, на странице API и SDK перечислены сгенерированные варианты.
Читайте серию основополагающих статей
Эта статья входит в кластер engineering. Начните с руководства по бесплатному API сокращателя URL, чтобы разобраться с форматом endpoint и авторизацией, а затем прочитайте о ограничениях скорости и идемпотентности, чтобы понять поведение под нагрузкой. Актуальная справочная информация находится в документации API.
Другие статьи в блоге
Частые вопросы
Как сократить URL в C#?
Отправьте длинный URL методом POST в 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 при регистрации клиента. Он добавляет повторы с джиттером в задержке, общий тайм-аут запроса и автоматический выключатель без кода в каждом вызове. В старых проектах ту же задачу решает AddTransientHttpErrorPolicy из Polly.
Как сократить сразу много URL в C#?
Используйте Parallel.ForEachAsync с MaxDegreeOfParallelism, равным примерно восьми: он ограничивает число выполняющихся запросов независимо от длины списка. Собирайте результаты в ConcurrentDictionary с ключом в виде исходного URL, чтобы частичный сбой оставался видимым, а пакет можно было запустить повторно.
Где хранить ключ API в приложении .NET?
В конфигурации, которая не попадает в систему контроля версий: в секретах пользователя при разработке, в переменных окружения или хранилище ключей в продакшене. Свяжите его с помощью IOptions и считывайте при регистрации клиента, чтобы замена ключа требовала изменения конфигурации, а не пересборки.
Попробуйте Elido
Вставьте URL - получите короткую ссылку
Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.
Бесплатно, без регистрации · 2 в день