Si estás construyendo un backend de pagos, un motor de facturación SaaS o un servicio empresarial de precios sobre el stack de Microsoft, integrar una API de tipos de cambio en C# / .NET es una de las tareas con mayor impacto que puedes enviar a producción esta semana. El .NET moderno te da todo lo que necesitas listo para usar — HttpClient, IHttpClientFactory, System.Text.Json, IMemoryCache y inyección de dependencias de primera clase — y aun así la mayoría de los tutoriales todavía muestran un único snippet con WebClient.DownloadString que no pasaría ninguna revisión de código en producción. Esta guía te lleva desde tu primera petición hasta un cliente tipado, cacheado y resiliente que puedes integrar en ASP.NET Core, una app Blazor, una herramienta de escritorio WPF o una Azure Function.
Al terminar este tutorial tendrás un cliente Finexly reutilizable para la documentación de la API de Finexly, un pequeño conversor de consola y una API mínima de ASP.NET Core que sirve tipos de cambio en tiempo real al resto de tu stack — todo escrito en .NET 8 / .NET 9 idiomático con los patrones que Microsoft mismo recomienda.
Por qué C# / .NET es una buena elección para integraciones de divisas
La conversión de divisas en producción es principalmente un problema de E/S: obtener tipos, cachearlos, multiplicar algunos decimales y devolver JSON. El runtime de .NET ha sido optimizado para exactamente esa carga durante la última década:
IHttpClientFactoryresuelve el agotamiento de sockets. Agrupa instancias deHttpMessageHandlerpara que puedas inyectarHttpClienten todas partes sin fugar sockets — la mayor fuente de fallos intermitentes de APIs de divisas que vemos en código .NET heredado.System.Text.Jsones rápido y de baja asignación. Deserializa una respuesta típica de 170 divisas en muy por debajo de un milisegundo, sin dependencias de terceros.decimalestá integrado en el lenguaje. Nunca necesitas unBigDecimalde terceros para manejar dinero correctamente — y eso importa cuando sumas miles de líneas de facturas convertidas.- Polly es la librería de resiliencia de facto. Reintentos, circuit breakers y timeouts son una sola llamada a
AddPolicyHandler. - APIs mínimas y AOT. Levantar un microservicio diminuto de tipos de cambio en 30 líneas es ahora realista, y arranca en frío en milisegundos.
Si estás evaluando otros stacks, tenemos guías paralelas para Node.js, Python, Go y PHP. Este artículo es específicamente para C# y asume que estás cómodo con async/await, hosting genérico e inyección de dependencias.
Requisitos previos y configuración del proyecto
Necesitas .NET 8 o .NET 9 instalado. Confirma tu SDK:
dotnet --versionCrea una nueva solución con una librería de clases para el cliente y una app de consola para ejercitarlo:
mkdir Finexly.Sample && cd Finexly.Sample
dotnet new sln -n Finexly.Sample
dotnet new classlib -n Finexly.Client
dotnet new console -n Finexly.ConsoleApp
dotnet sln add Finexly.Client/Finexly.Client.csproj
dotnet sln add Finexly.ConsoleApp/Finexly.ConsoleApp.csproj
dotnet add Finexly.ConsoleApp/Finexly.ConsoleApp.csproj reference Finexly.Client/Finexly.Client.csprojTambién necesitas una API key de Finexly. Regístrate gratis — sin tarjeta de crédito, y el plan gratuito incluye 1.000 peticiones al mes, suficientes para desarrollo y CI.
Nunca codifiques la clave en el código. Guárdala como variable de entorno:
# macOS / Linux
export FINEXLY_API_KEY="your_api_key_here"# Windows PowerShell
$env:FINEXLY_API_KEY = "your_api_key_here"En producción, guárdala en Azure Key Vault, AWS Secrets Manager o dotnet user-secrets para desarrollo local.
Tu primera petición con HttpClient
Empecemos con el ejemplo funcional más simple. Abre Finexly.ConsoleApp/Program.cs y escribe:
using System.Net.Http;
using System.Net.Http.Json;
var apiKey = Environment.GetEnvironmentVariable("FINEXLY_API_KEY")
?? throw new InvalidOperationException("FINEXLY_API_KEY not set");
using var http = new HttpClient
{
BaseAddress = new Uri("https://api.finexly.com/v1/")
};
var url = $"latest?base=USD&apikey={apiKey}";
var payload = await http.GetFromJsonAsync<Dictionary<string, object>>(url);
Console.WriteLine(payload?["rates"]);Ejecútalo:
dotnet run --project Finexly.ConsoleAppVerás un diccionario de códigos de divisa y sus tipos de cambio contra USD. Funciona, pero tiene todos los olores que esperarías de un ejemplo de 10 líneas: valores object sin tipo, sin caché, sin reintentos, un HttpClient instanciado manualmente y la API key en la URL. Vamos a arreglar todo eso.
Definiendo modelos de respuesta fuertemente tipados
System.Text.Json deserializa de forma limpia en tipos record de C#. En el proyecto Finexly.Client, crea Models/LatestRatesResponse.cs:
using System.Text.Json.Serialization;
namespace Finexly.Client.Models;
public sealed record LatestRatesResponse(
[property: JsonPropertyName("base")] string Base,
[property: JsonPropertyName("date")] DateOnly Date,
[property: JsonPropertyName("rates")] IReadOnlyDictionary<string, decimal> Rates
);Algunos detalles a destacar:
decimalen lugar dedouble. El error de punto flotante es inaceptable para dinero.decimalte da 28–29 dígitos significativos y aritmética exacta en base 10.IReadOnlyDictionary. La respuesta es de solo lectura — exprésalo en el tipo.DateOnly. El .NET moderno trae un tipo de fecha apropiado. Úsalo en lugar deDateTimecuando no haya componente horario.
Ahora podemos deserializar sin gimnasia con dynamic ni Dictionary<string, object>.
Construyendo un HttpClient tipado con IHttpClientFactory
El patrón recomendado en .NET es un cliente tipado registrado a través de IHttpClientFactory. Te da pooling de conexiones, handlers configurables y DI fácil. Crea FinexlyClient.cs en el proyecto Finexly.Client:
using System.Net.Http.Json;
using Finexly.Client.Models;
namespace Finexly.Client;
public sealed class FinexlyClient
{
private readonly HttpClient _http;
private readonly string _apiKey;
public FinexlyClient(HttpClient http, FinexlyOptions options)
{
_http = http;
_apiKey = options.ApiKey;
_http.BaseAddress = new Uri("https://api.finexly.com/v1/");
_http.DefaultRequestHeaders.Add("X-Api-Key", _apiKey);
_http.Timeout = TimeSpan.FromSeconds(5);
}
public async Task<LatestRatesResponse> GetLatestRatesAsync(
string baseCurrency = "USD",
IEnumerable<string>? symbols = null,
CancellationToken cancellationToken = default)
{
var query = $"latest?base={baseCurrency}";
if (symbols is not null)
{
query += $"&symbols={string.Join(",", symbols)}";
}
var response = await _http.GetFromJsonAsync<LatestRatesResponse>(
query, cancellationToken);
return response
?? throw new InvalidOperationException("Empty response from Finexly");
}
public async Task<decimal> ConvertAsync(
string from, string to, decimal amount,
CancellationToken cancellationToken = default)
{
var rates = await GetLatestRatesAsync(from, new[] { to }, cancellationToken);
if (!rates.Rates.TryGetValue(to, out var rate))
{
throw new ArgumentException($"Unknown currency: {to}");
}
return amount * rate;
}
}
public sealed class FinexlyOptions
{
public required string ApiKey { get; init; }
}Algunos puntos a destacar:
- La API key va en una cabecera, no en la URL — más seguro para logs, proxies y trazas.
- Un
Timeoutde 5 segundos evita que una API de divisas atascada se propague en un checkout lento. - El cliente acepta un
CancellationTokenen cada método — esto es innegociable en ASP.NET moderno. - Usa
GetFromJsonAsync<T>, que combina el GET, la verificación de estado y la deserialización en una sola llamada con pocas asignaciones.
Registrando el cliente en inyección de dependencias
En tu Program.cs (Consola, ASP.NET Core, Worker — misma API en todas partes), conéctalo:
using Finexly.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton(new FinexlyOptions
{
ApiKey = Environment.GetEnvironmentVariable("FINEXLY_API_KEY")!
});
builder.Services.AddHttpClient<FinexlyClient>();
using var host = builder.Build();
var finexly = host.Services.GetRequiredService<FinexlyClient>();
var eur = await finexly.ConvertAsync("USD", "EUR", 100m);
Console.WriteLine($"100 USD = {eur:0.00} EUR");AddHttpClient<FinexlyClient>() registra FinexlyClient como servicio transient y le da un HttpClient pooled desde la factory. Ahora puedes inyectar FinexlyClient en cualquier controlador, endpoint de API mínima o servicio en segundo plano.
Añadiendo reintentos y circuit breaker con Polly
Las APIs externas a veces pierden una petición, hacen throttling o tienen un hipo a nivel de red. Polly es la librería de resiliencia estándar en .NET y se integra con IHttpClientFactory en dos líneas.
Instala el paquete:
dotnet add Finexly.ConsoleApp package Microsoft.Extensions.Http.PollyConfigura un reintento con backoff y un circuit breaker:
using Polly;
using Polly.Extensions.Http;
builder.Services.AddHttpClient<FinexlyClient>()
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(r => (int)r.StatusCode == 429)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt =>
TimeSpan.FromMilliseconds(200 * Math.Pow(2, attempt))))
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30)));Este único registro reintenta en 5xx, 408 y 429 con backoff exponencial (200ms, 400ms, 800ms), y dispara un circuit breaker si la API está totalmente caída — para que tu página de checkout degrade con elegancia en lugar de mantener conexiones abiertas durante minutos.
Cacheando respuestas con IMemoryCache
Los tipos de cambio en vivo rara vez cambian más de una vez por minuto, así que una pequeña caché en proceso reduce las llamadas upstream en un 95% o más. .NET trae IMemoryCache listo para usar.
dotnet add Finexly.Client package Microsoft.Extensions.Caching.MemoryCrea CachedFinexlyClient.cs:
using Finexly.Client.Models;
using Microsoft.Extensions.Caching.Memory;
namespace Finexly.Client;
public sealed class CachedFinexlyClient
{
private readonly FinexlyClient _inner;
private readonly IMemoryCache _cache;
private static readonly TimeSpan DefaultTtl = TimeSpan.FromSeconds(60);
public CachedFinexlyClient(FinexlyClient inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}
public Task<LatestRatesResponse> GetLatestRatesAsync(
string baseCurrency,
CancellationToken cancellationToken = default)
{
var key = $"finexly:latest:{baseCurrency}";
return _cache.GetOrCreateAsync(key, entry =>
{
entry.AbsoluteExpirationRelativeToNow = DefaultTtl;
return _inner.GetLatestRatesAsync(baseCurrency,
cancellationToken: cancellationToken);
})!;
}
}Regístralo junto a IMemoryCache:
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<CachedFinexlyClient>();Un TTL de 60 segundos es un excelente valor por defecto para precios de checkout, vistas previas de facturación y dashboards de admin. Para datos históricos, sube el TTL a horas o días — los tipos históricos no cambian.
Un conversor de divisas de consola funcional
Juntándolo todo, aquí tienes una pequeña CLI que imita currencyconverter --amount 100 --from USD --to JPY:
using Finexly.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddSingleton(new FinexlyOptions
{
ApiKey = Environment.GetEnvironmentVariable("FINEXLY_API_KEY")!
});
builder.Services.AddHttpClient<FinexlyClient>();
builder.Services.AddSingleton<CachedFinexlyClient>();
using var host = builder.Build();
decimal amount = 1m;
string from = "USD", to = "EUR";
for (int i = 0; i < args.Length - 1; i++)
{
switch (args[i])
{
case "--amount": amount = decimal.Parse(args[i + 1]); break;
case "--from": from = args[i + 1].ToUpperInvariant(); break;
case "--to": to = args[i + 1].ToUpperInvariant(); break;
}
}
var finexly = host.Services.GetRequiredService<CachedFinexlyClient>();
var rates = await finexly.GetLatestRatesAsync(from);
if (!rates.Rates.TryGetValue(to, out var rate))
{
Console.Error.WriteLine($"Unknown currency: {to}");
return 1;
}
Console.WriteLine($"{amount:0.00} {from} = {amount * rate:0.00} {to}");
return 0;Compila un binario de un solo archivo que puedes dejar en cualquier servidor:
dotnet publish Finexly.ConsoleApp -c Release -r linux-x64 \
--self-contained -p:PublishSingleFile=trueExponiendo tipos de cambio con una API mínima de ASP.NET Core
Para la mayoría de los equipos, la arquitectura correcta es un microservicio interno de tipos que envuelve la API upstream, añade caché y da a todos los demás servicios un endpoint rápido, gratuito e in-VPC. Las APIs mínimas de ASP.NET Core convierten esto en un archivo de 30 líneas:
using Finexly.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddSingleton(new FinexlyOptions
{
ApiKey = builder.Configuration["Finexly:ApiKey"]!
});
builder.Services.AddHttpClient<FinexlyClient>();
builder.Services.AddSingleton<CachedFinexlyClient>();
var app = builder.Build();
app.MapGet("/rates", async (
string? @base,
CachedFinexlyClient finexly,
CancellationToken ct) =>
{
var rates = await finexly.GetLatestRatesAsync(@base ?? "USD", ct);
return Results.Ok(rates);
});
app.MapGet("/convert", async (
string from, string to, decimal amount,
CachedFinexlyClient finexly,
CancellationToken ct) =>
{
var rates = await finexly.GetLatestRatesAsync(from, ct);
return rates.Rates.TryGetValue(to, out var rate)
? Results.Ok(new { from, to, amount, converted = amount * rate })
: Results.NotFound(new { error = $"Unknown currency {to}" });
});
app.Run();Despliega esto detrás de tu API gateway habitual y cualquier servicio de tu flota podrá llamar GET /convert?from=USD&to=EUR&amount=99.95 con latencia inferior a 10 milisegundos en un cache hit. Para una experiencia en el navegador, los mismos datos están disponibles en nuestro conversor de divisas alojado.
Manejando dinero correctamente en C#
Algunos patrones defensivos que se pagan solos la primera vez que auditas un libro mayor de pagos:
- Usa siempre
decimal, nuncadoublenifloat.0.1 + 0.2famosamente no es0.3en coma flotante — y ese único error de redondeo puede costarte una disputa de contracargo. - Redondea en presentación, no en cálculo. Mantén precisión total a través de cada multiplicación y solo
Math.Round(value, 2, MidpointRounding.ToEven)cuando muestres o persistas. - Fija el modo de redondeo. El redondeo bancario (
ToEven) es el estándar de la industria financiera en IEEE 754 e ISO 80000-1. - Trata las divisas como códigos opacos. Usa cadenas ISO 4217 — nunca asumas que una divisa tiene dos decimales. JPY tiene cero, BHD tiene tres. Nuestra guía ISO 4217 cubre los casos extremos.
Buenas prácticas para producción
Algunos patrones adicionales separan una integración hobby de un sistema que puedes mantener durante años:
- Configura timeouts HTTP ajustados. Un
HttpClient.Timeoutpor defecto de 100 segundos bloqueará tu thread pool bajo carga. Cinco segundos es de sobra para una API de divisas. - Envuelve cada llamada externa en un
CancellationToken. ASP.NET Core te pasa uno gratis; respétalo para que las peticiones abortadas no sigan hablando con el upstream. - Loguea códigos de estado, no cuerpos. Las respuestas de la API de divisas son demasiado grandes para loguear y pueden contener datos que no quieres en Splunk.
- Monitoriza tu cuota. Compara planes de precios y configura alertas en CloudWatch / Application Insights al 80% de tu límite mensual.
- Fija las divisas que realmente usas. Enviar
symbols=EUR,GBP,JPYreduce la respuesta de ~10 KB a menos de 200 bytes. - Cachea de forma agresiva. Los checkouts sensibles a la latencia pueden correr con una caché de 30 segundos; los dashboards de admin pueden correr con 5 minutos.
Si estás evaluando proveedores, nuestra comparativa de APIs de divisas gratuitas vs de pago recorre precisión, uptime y precios entre las principales opciones.
Preguntas frecuentes
¿Qué API de divisas funciona mejor con C# y .NET?
Cualquier API REST con JSON limpio y uptime estable se integrará bien vía IHttpClientFactory. Finexly está específicamente diseñada para ser agnóstica del lenguaje — los mismos endpoints son consumidos por clientes Node, Python, Go, Java y .NET sin lock-in de SDK. Consulta nuestra visión general de la API gratuita de divisas para más contexto.
¿Debería usar Newtonsoft.Json o System.Text.Json?
Para código nuevo en .NET 6+, usa System.Text.Json. Es 2–3× más rápido, asigna menos memoria y forma parte de la biblioteca base, así que no hay un paquete extra que mantener actualizado. Newtonsoft.Json sigue siendo excelente y vale la pena conservarlo en código legacy, pero las integraciones nuevas de divisas en C# deberían usar System.Text.Json por defecto.
¿Necesito un SDK de NuGet o basta con HttpClient? Un cliente tipado escrito a mano (unas 50 líneas de C#) es casi siempre la respuesta correcta. Te da control total sobre caché, reintentos, telemetría y serialización, y evita el riesgo de desincronización de versiones que viene con cualquier SDK de terceros. El ejemplo de este tutorial está listo para producción.
¿Con qué frecuencia debería refrescar los tipos de cambio en una aplicación .NET? Para la mayoría de casos — checkouts de e-commerce, facturación SaaS, facturación — una caché de 30 a 60 segundos es segura. Los sistemas de trading obviamente necesitan datos en tiempo real, pero para todo lo demás, la frescura a nivel de minuto es invisible para los usuarios y reduce drásticamente el gasto en API.
¿Puedo ejecutar esto en Azure Functions o AWS Lambda?
Sí. IHttpClientFactory funciona de la misma manera en Azure Functions de proceso aislado y en handlers Lambda con Microsoft.Extensions.Hosting. Para cargas sensibles al arranque en frío, publica con AOT (PublishAot=true) y tu función arrancará en menos de 100 ms.
¿Es decimal lento comparado con double?
decimal es aproximadamente 10–20× más lento que double por operación aritmética, pero una conversión de divisas hace como mucho un puñado de multiplicaciones. Nunca medirás la diferencia en una carga real — y la ganancia de corrección es innegociable para dinero.
Resumen
Ahora tienes un plano completo y listo para producción para integrar una API de tipos de cambio en C# / .NET: modelos fuertemente tipados, un cliente respaldado por IHttpClientFactory, reintentos y circuit breaker basados en Polly, caché en proceso, un conversor de consola y una API mínima de ASP.NET Core. Cada patrón aquí sale directamente de la guía oficial de Microsoft y está probado en código real de fintech en producción.
¿Listo para enviar tipos de cambio en tiempo real a tu aplicación .NET? Consigue tu API key gratuita de Finexly — sin tarjeta de crédito. Empieza con 1.000 peticiones gratis al mes y escala a medida que crezca tu producto. Si primero quieres ver cómo se compara Finexly con otros proveedores, lee nuestra comparativa de APIs de divisas.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →