Acortar una URL en C# requiere un solo POST. Envía el destino a la API del acortador con HttpClient, pasa tu clave de API como token Bearer y deserializa short_url de la respuesta. PostAsJsonAsync se encarga de la serialización, así que, una vez registrado el cliente, la llamada ocupa una sola línea.
La forma del endpoint, el modelo de autenticación y los límites del plan gratuito que se dan por supuestos aquí están documentados en la guía general de la API gratuita de acortamiento de URL. Esta es la entrega sobre C# de una serie que también cubre Python, JavaScript, Go y Java.
La forma más 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
Los nombres de las propiedades del record coinciden deliberadamente con el snake_case de la API, lo que mantiene el ejemplo sin dependencias. En un proyecto real, añade [JsonPropertyName("short_url")] y ponle a la propiedad un nombre de C#, o establece PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower en las opciones una sola vez.
EnsureSuccessStatusCode es la línea importante. Sin ella, un 401 se completa felizmente, ReadFromJsonAsync devuelve un record con valores nulos y el fallo aparece tres capas más abajo como una NullReferenceException en algo que no tiene relación.
Registra el cliente en lugar de crear uno nuevo
El fragmento anterior está bien para una prueba rápida en consola, pero es incorrecto en un servicio. Un HttpClient creado en cada llamada abre cada vez un grupo de conexiones nuevo y, al desecharlo, deja el socket en TIME_WAIT; si lo haces en un bucle, te quedas sin puertos. IHttpClientFactory agrupa los handlers y rota el DNS correctamente.
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 procede de Microsoft.Extensions.Http.Resilience y te proporciona reintentos con jitter ante fallos transitorios, un tiempo de espera total para la solicitud y un disyuntor sin que tengas que escribir una política. En un proyecto antiguo, AddTransientHttpErrorPolicy de Polly cubre lo mismo.
El cliente tipado en sí se mantiene pequeño:
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;
}
}
Esa cabecera es la razón por la que es seguro activar el controlador de resiliencia. Sin ella, un reintento después de perder la respuesta crea un segundo enlace para el mismo destino; con ella, la API devuelve el original. La guía detallada sobre límites de velocidad e idempotencia explica por qué derivar la clave del destino es mejor que usar un GUID nuevo para el trabajo por lotes.
¿Quieres probarlo contra un endpoint activo? Crea una clave en el plan gratuito, guárdala en los secretos de usuario como Elido:ApiKey y el registro anterior funcionará sin cambios.
Lee el cuerpo del error en lugar de adivinar
EnsureSuccessStatusCode es el valor predeterminado correcto, pero descarta la parte más útil de una llamada fallida. El mensaje de la excepción contiene el código de estado y nada más, así que un 422 te dice que la solicitud se rechazó, pero no qué campo hizo que la API la rechazara.
Para cualquier cosa que tengas que depurar a las tres de la mañana, lee primero el cuerpo:
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}"),
};
}
De ahí se desprenden dos cosas. La línea del registro nombra el campo que produjo el error de validación, en lugar de dejarte reconstruir la solicitud, y el switch separa los fallos que merece la pena reintentar de los que nunca tendrán éxito. Reintentar tres veces un 401 son tres llamadas desperdiciadas y un fallo más lento.
El controlador de resiliencia estándar ya trata los códigos 5xx y 429 como transitorios y deja intactos los 4xx, así que las dos capas coinciden: reintenta lo que merece la pena reintentar y tu código explica lo que queda.
En bloque sin superar el límite de velocidad
Task.WhenAll sobre una lista de 3.000 URL inicia 3.000 solicitudes. Parallel.ForEachAsync limita cuántas están en curso y se lee mejor que un semáforo escrito a mano:
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
}
});
Ocho es un punto de partida, no una constante. Vigila los 429 y la cabecera Retry-After que envía la API, y reduce el valor en lugar de aumentarlo. Usar la URL original como clave significa que una ejecución a medias te indica exactamente qué filas debes repetir.
Dónde debería vivir el código
Una herramienta de consola que acorta un CSV, un servicio hospedado que crea un enlace cuando se envía un pedido, una Azure Function activada por una cola: el mismo cliente tipado, tres registros distintos. Lo único que no debe variar es de dónde sale la clave. Secretos de usuario en desarrollo, variables de entorno o un almacén de claves en producción, IOptions entre ambos y nada en appsettings.json que termine en el repositorio.
Cuando los enlaces se crean desde un servicio, qué más expone la API para desarrolladores se convierte en la parte interesante, y los webhooks para eventos de enlaces son mejores que consultar periódicamente los datos de clics y escaneos. Si prefieres no mantener el cliente, la página de la API y los SDK enumera los generados.
Lee la serie principal
Esto forma parte del clúster de ingeniería. Empieza con la guía de la API gratuita de acortamiento de URL para conocer la forma del endpoint y la autenticación; después consulta límites de velocidad e idempotencia para el comportamiento bajo carga. La referencia activa está en la documentación de la API.
Relacionado en el blog
Preguntas frecuentes
¿Cómo acorto una URL en C#?
Envía la URL larga mediante POST a la API de un acortador con HttpClient, usando PostAsJsonAsync para que el cuerpo se serialice automáticamente; después, deserializa la respuesta en un record pequeño y lee ShortUrl. Con la cabecera Authorization configurada en el cliente, la llamada en sí ocupa una sola línea.
¿Por qué no debería usar new HttpClient() en un bucle?
Cada instancia abre su propio grupo de conexiones y, al desecharla, deja el socket en TIME_WAIT, así que un bucle puede agotar los puertos disponibles bajo carga. Registra el cliente con IHttpClientFactory e inyéctalo: agrupa los handlers y rota el DNS correctamente.
¿HttpClient lanza una excepción con una respuesta 401 o 500?
No de forma predeterminada. La tarea se completa correctamente e IsSuccessStatusCode es false, así que una llamada fallida parece funcionar a menos que lo compruebes. Llama a EnsureSuccessStatusCode para convertirla en una HttpRequestException o comprueba el estado tú mismo antes de leer el cuerpo.
¿Cómo añado reintentos a HttpClient en .NET?
Añade el controlador de resiliencia estándar de Microsoft.Extensions.Http.Resilience al registro del cliente. Incluye reintentos con backoff y jitter, un tiempo de espera total para la solicitud y un disyuntor sin código por llamada. En proyectos antiguos, AddTransientHttpErrorPolicy de Polly hace el mismo trabajo.
¿Cómo acorto muchas URL a la vez en C#?
Usa Parallel.ForEachAsync con MaxDegreeOfParallelism establecido aproximadamente en ocho, lo que limita cuántas solicitudes están en curso independientemente de la longitud de la lista. Recopila los resultados en un ConcurrentDictionary cuya clave sea la URL original para que un fallo parcial siga siendo visible y el lote se pueda volver a ejecutar.
¿Dónde debería estar la clave de API en una aplicación .NET?
En una configuración que no se registre en el repositorio: secretos de usuario en desarrollo, variables de entorno o un almacén de claves en producción. Vincúlala con IOptions y léela durante el registro del cliente, para que rotar una clave implique cambiar la configuración en lugar de recompilar.
Prueba Elido
Pega una URL, obtén un enlace corto
Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.
Gratis, sin registro · 2 por día