Назад к блогу

API курсов валют в C# / .NET — Полный учебник по интеграции (2026)

V
Vlado Grigirov
May 26, 2026
C# .NET Currency API Exchange Rates Tutorial Fintech ASP.NET Core

Если вы строите платёжный бэкенд, движок биллинга для 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 курсов.

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 →