Если вы строите платёжный бэкенд, движок биллинга для SaaS или корпоративный сервис ценообразования на стеке Microsoft, интеграция API курсов валют в C# / .NET — одна из задач с наибольшей отдачей, которую вы можете выкатить на этой неделе. Современный .NET даёт вам всё нужное прямо из коробки — HttpClient, IHttpClientFactory, System.Text.Json, IMemoryCache и DI первого класса, — однако большинство туториалов до сих пор показывают единственный сниппет с WebClient.DownloadString, который не пройдёт ни одного производственного код-ревью. Этот гайд проведёт вас от первого запроса до типобезопасного, кэшированного, отказоустойчивого клиента, который можно встроить в ASP.NET Core, Blazor-приложение, WPF-инструмент или Azure Function.
К концу учебника у вас будет переиспользуемый клиент Finexly для документации API Finexly, маленький консольный конвертер и ASP.NET Core Minimal API, отдающий курсы валют в реальном времени остальной части стека, — всё это написано в идиоматичном .NET 8 / .NET 9 по тем паттернам, которые рекомендует сама Microsoft.
Почему C# / .NET — сильный выбор для валютных интеграций
Конвертация валют в продакшене — в первую очередь задача I/O: получить курсы, закэшировать, перемножить десятичные дроби, отдать JSON. Рантайм .NET последние десять лет оптимизировался именно под такую нагрузку:
IHttpClientFactoryрешает проблему истощения сокетов. Он пулит экземплярыHttpMessageHandler, поэтому вы можете инжектитьHttpClientкуда угодно без утечек сокетов — главная причина периодических сбоев валютных API, которую мы видим в легаси-коде .NET.System.Text.Jsonбыстрый и экономный по аллокациям. Десериализует типичный ответ на 170 валют существенно меньше чем за миллисекунду, без сторонних зависимостей.decimalвстроен в язык. СтороннийBigDecimalникогда не нужен для корректной работы с деньгами — и это важно, когда вы суммируете тысячи строк сконвертированных счетов.- Polly — фактический стандарт устойчивости. Повторы, circuit breaker и таймауты — один вызов
AddPolicyHandler. - Minimal API и AOT. Поднять крошечный микросервис курсов в 30 строк теперь реалистично, и он холодно стартует за миллисекунды.
Если вы выбираете между стеками, есть параллельные гайды для Node.js, Python, Go и PHP. Эта статья — специально для C# и предполагает, что вам комфортно с async/await, Generic Hosting и DI.
Предварительные требования и настройка проекта
Нужен .NET 8 или .NET 9. Проверьте SDK:
dotnet --versionСоздайте решение с библиотекой классов для клиента и консольным приложением для проверки:
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.csprojЕщё нужен API-ключ Finexly. Зарегистрируйтесь бесплатно — без банковской карты, бесплатный тариф даёт 1 000 запросов в месяц, чего более чем достаточно для разработки и CI.
Никогда не зашивайте ключ в код. Положите его в переменную окружения:
# macOS / Linux
export FINEXLY_API_KEY="your_api_key_here"# Windows PowerShell
$env:FINEXLY_API_KEY = "your_api_key_here"В продакшене храните его в Azure Key Vault, AWS Secrets Manager или dotnet user-secrets для локальной разработки.
Первый запрос через HttpClient
Начнём с самого простого рабочего примера. Откройте Finexly.ConsoleApp/Program.cs:
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"]);Запустите:
dotnet run --project Finexly.ConsoleAppВы увидите словарь кодов валют и их курсы к USD. Это работает, но у такого 10-строчного примера все ожидаемые «запахи»: значения типа object без типизации, нет кэша, нет повторов, ручной HttpClient и API-ключ в URL. Сейчас всё это исправим.
Определение строго типизированных моделей ответа
System.Text.Json чисто десериализует в типы record C#. В проекте Finexly.Client создайте 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
);Несколько важных деталей:
decimalвместоdouble. Ошибка с плавающей точкой недопустима для денег.decimalдаёт 28–29 значащих цифр и точную десятичную арифметику.IReadOnlyDictionary. Ответ только для чтения — выразите это в типе.DateOnly. В современном .NET есть нормальный тип даты. Используйте его вместоDateTime, если нет времени.
Теперь можно десериализовать без гимнастики с dynamic и Dictionary<string, object>.
Сборка типизированного HttpClient через IHttpClientFactory
Рекомендуемый паттерн в .NET — типизированный клиент, зарегистрированный через IHttpClientFactory. Он даёт пулинг соединений, настраиваемые handler-ы и удобный DI. Создайте FinexlyClient.cs в проекте 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; }
}Несколько моментов:
- API-ключ уходит в заголовок, а не в URL — безопаснее для логов, прокси и трейсинга.
Timeoutв 5 секунд не даст зависшему API курсов превратить чекаут в медленный.- Клиент принимает
CancellationTokenв каждом методе — в современном ASP.NET это не обсуждается. - Используется
GetFromJsonAsync<T>, который объединяет GET, проверку статуса и десериализацию в одном дружественном к аллокациям вызове.
Регистрация клиента в Dependency Injection
В Program.cs (Console, ASP.NET Core, Worker — везде один API) подключите:
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>() регистрирует FinexlyClient как transient и выдаёт ему пуленный HttpClient из фабрики. Теперь FinexlyClient можно инжектить в любой контроллер, эндпоинт Minimal API или фоновый сервис.
Добавляем повторы и circuit breaker через Polly
Внешние API иногда теряют запрос, тротлят или икают на сетевом уровне. Polly — стандартная библиотека устойчивости в .NET и интегрируется с IHttpClientFactory в две строки.
Установите пакет:
dotnet add Finexly.ConsoleApp package Microsoft.Extensions.Http.PollyНастройте повтор с backoff и 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)));Эта одна регистрация повторяет на 5xx, 408 и 429 с экспоненциальным backoff (200мс, 400мс, 800мс) и срывает circuit breaker, если API полностью лежит, — страница чекаута деградирует красиво вместо того, чтобы держать соединения открытыми минутами.
Кэшируем ответы через IMemoryCache
Курсы в реальном времени редко меняются чаще раза в минуту, так что небольшой in-process кэш обычно сокращает обращения к upstream на 95% и более. IMemoryCache поставляется в коробке.
dotnet add Finexly.Client package Microsoft.Extensions.Caching.MemoryСоздайте 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);
})!;
}
}Зарегистрируйте вместе с IMemoryCache:
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<CachedFinexlyClient>();TTL в 60 секунд — отличный дефолт для цен на чекауте, превью биллинга и админ-дашбордов. Для исторических данных TTL можно увеличить до часов или дней — историческое не меняется.
Рабочий консольный конвертер валют
Складывая всё вместе, получаем небольшой CLI вида 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;Соберите single-file бинарь, который можно положить на любой сервер:
dotnet publish Finexly.ConsoleApp -c Release -r linux-x64 \
--self-contained -p:PublishSingleFile=trueОтдаём курсы через ASP.NET Core Minimal API
Для большинства команд правильная архитектура — внутренний микросервис курсов, который оборачивает upstream API, добавляет кэш и даёт остальным сервисам быстрый, бесплатный, in-VPC эндпоинт. Minimal API в ASP.NET Core превращает это в файл из 30 строк:
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();Развернёте это за своим обычным API-шлюзом — и любой сервис флота сможет вызывать GET /convert?from=USD&to=EUR&amount=99.95 с задержкой меньше 10 миллисекунд при попадании в кэш. Для браузерного опыта те же данные доступны в нашем хостед конвертере валют.
Корректная работа с деньгами в C#
Несколько защитных паттернов, которые окупятся в первый же аудит платёжной книги:
- Всегда используйте
decimal, никогдаdoubleилиfloat.0.1 + 0.2известно не равно0.3в плавающей точке — и эта одна ошибка округления может стоить вам спора по chargeback. - Округляйте при отображении, не при вычислении. Держите полную точность через каждое умножение и только при выводе/сохранении
Math.Round(value, 2, MidpointRounding.ToEven). - Фиксируйте режим округления. Банкирское округление (
ToEven) — финансовый стандарт в IEEE 754 и ISO 80000-1. - Считайте валюты непрозрачными кодами. Используйте строки ISO 4217 и не предполагайте, что у валюты две дробных цифры. У JPY — ноль, у BHD — три. Граничные случаи разобраны в нашем гайде по ISO 4217.
Лучшие практики для продакшена
Дополнительные паттерны, отделяющие хобби-интеграцию от системы, которую можно эксплуатировать годами:
- Ставьте жёсткие HTTP-таймауты. Дефолтный
HttpClient.Timeoutв 100 секунд заблокирует thread pool под нагрузкой. Для API курсов пяти секунд более чем достаточно. - Заворачивайте каждый внешний вызов в
CancellationToken. ASP.NET Core передаёт его бесплатно; уважайте его, чтобы оборванные запросы не продолжали говорить с upstream. - Логируйте коды состояния, а не тела. Ответы API курсов слишком большие, чтобы их полезно логировать, и могут содержать данные курсов, которые вы не хотите хранить в Splunk.
- Мониторьте квоту. Сравните тарифные планы и настройте алерты в CloudWatch / Application Insights на 80% месячного лимита.
- Пиньте только нужные валюты. Отправка
symbols=EUR,GBP,JPYуменьшает ответ с ~10 КБ до менее 200 байт. - Кэшируйте агрессивно. Чекауты, чувствительные к задержке, проживут на 30-секундном кэше; админ-дашборды — на 5-минутном.
Если вы выбираете провайдера, наше сравнение бесплатных и платных API курсов рассматривает точность, аптайм и цены ведущих опций.
Часто задаваемые вопросы
Какой API курсов лучше всего работает с C# и .NET?
Любой REST API с чистым JSON и стабильным аптаймом хорошо интегрируется через IHttpClientFactory. Finexly специально спроектирован как языко-агностичный — те же эндпоинты потребляются клиентами Node, Python, Go, Java и .NET без привязки к SDK. Подробнее — в обзоре бесплатного API курсов.
Newtonsoft.Json или System.Text.Json?
Для нового кода на .NET 6+ — System.Text.Json. Он в 2–3 раза быстрее, аллоцирует меньше и входит в BCL, так что нет дополнительного пакета для поддержки. Newtonsoft.Json по-прежнему отличный и заслуживает места в легаси-коде, но новые валютные интеграции на C# должны по умолчанию использовать System.Text.Json.
Нужен ли NuGet-SDK или хватит HttpClient? Самописный типизированный клиент (около 50 строк C#) почти всегда правильный ответ. Он даёт полный контроль над кэшированием, повторами, телеметрией и сериализацией и убирает риск рассинхронизации версий, присущий любому стороннему SDK. Пример из этого учебника готов к продакшену.
Как часто обновлять курсы в .NET-приложении? Для большинства сценариев — чекауты в e-commerce, биллинг SaaS, выставление счетов — безопасен кэш на 30–60 секунд. Торговым системам, очевидно, нужны данные реального времени, но для всего остального минутная свежесть невидима для пользователя и резко снижает расходы на API.
Можно ли запускать это в Azure Functions или AWS Lambda?
Да. IHttpClientFactory одинаково работает в изолированных Azure Functions и в Lambda-хендлерах с Microsoft.Extensions.Hosting. Для нагрузок, чувствительных к холодному старту, публикуйте с AOT (PublishAot=true), и функция запустится менее чем за 100 мс.
decimal медленный по сравнению с double?
decimal примерно в 10–20 раз медленнее double на арифметическую операцию, но конверсия валют делает максимум пару умножений. В реальной нагрузке разницы вы не измерите — а корректность для денег не обсуждается.
Заключение
Теперь у вас есть полная, готовая к продакшену схема для интеграции API курсов валют в C# / .NET: строго типизированные модели, клиент на базе IHttpClientFactory, повторы и circuit breaker через Polly, in-process кэш, консольный конвертер и ASP.NET Core Minimal API. Каждый паттерн взят прямо из официальных рекомендаций Microsoft и обкатан в реальном фин-tex продакшене.
Готовы доставить курсы в реальном времени в своё .NET-приложение? Получите бесплатный API-ключ Finexly — без банковской карты. Начните с 1 000 бесплатных запросов в месяц и апгрейдитесь, когда продукт вырастет. Если хотите сначала увидеть, как Finexly смотрится на фоне других провайдеров, прочитайте наше сравнение API курсов.
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 →