Tutorial/C# and .NET

Exchange rates in C# and .NET

Create a typed .NET FX client with IHttpClientFactory, decimal rates, cancellation, a bounded timeout, and source-aware response handling.

MMexchangerate.dev·Jul 17, 2026·6 min read

Register one typed HttpClient for exchangerate.dev, inject the API key from configuration, and deserialize rate values to decimal. Keep cancellation and timeout behavior explicit so a slow upstream request cannot hold an application request open indefinitely.

Key points
Use IHttpClientFactory for connection reuse, DNS refresh, logging, and central client configuration.
Deserialize rates to decimal and retain the API freshness metadata alongside them.
Pass a CancellationToken through every request and set a bounded client timeout.
Call EnsureSuccessStatusCode() before decoding so failed responses never look like valid rate data.

Register a typed HttpClient

The registration owns the base URL, bearer token, and timeout. The typed client owns the response contract and passes cancellation from the caller to the network request.

csharp · Program.cs and FxClient.cscopy
// Program.cs
using System.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient<FxClient>(client =>
{
    var key = builder.Configuration["EXCHANGERATE_API_KEY"]
        ?? throw new InvalidOperationException("EXCHANGERATE_API_KEY is missing");

    client.BaseAddress = new Uri("https://api.exchangerate.dev/v1/");
    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", key);
    client.Timeout = TimeSpan.FromSeconds(5);
});

// FxClient.cs
using System.Net.Http.Json;
using System.Text.Json.Serialization;

public sealed record LatestRates(
    string Base,
    string Source,
    [property: JsonPropertyName("market_session")] string MarketSession,
    [property: JsonPropertyName("data_updated_at")] DateTimeOffset DataUpdatedAt,
    Dictionary<string, decimal> Rates
);

public sealed class FxClient(HttpClient http)
{
    public async Task<LatestRates> GetLatestAsync(
        string baseCurrency,
        CancellationToken cancellationToken)
    {
        using var response = await http.GetAsync(
            $"latest/{baseCurrency}?symbols=EUR,GBP",
            cancellationToken);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<LatestRates>(
            cancellationToken: cancellationToken)
            ?? throw new InvalidOperationException("Empty FX response");
    }
}

Pass cancellation from the request

csharp · endpoint usagecopy
var app = builder.Build();

app.MapGet("/fx", async (
    FxClient fx,
    CancellationToken cancellationToken) =>
{
    var rates = await fx.GetLatestAsync("USD", cancellationToken);

    return Results.Ok(new {
        eur = rates.Rates["EUR"],
        rates.Source,
        rates.MarketSession,
        rates.DataUpdatedAt,
    });
});

app.Run();

Cache successful observations only

A shared cache can absorb repeated reads, but the cache entry should contain the complete observation rather than only Rates["EUR"]. Keep Source, MarketSession, and DataUpdatedAt so the caller can distinguish a live trading-day update from a daily or closed-market value.

Retries need a narrow policy
Retry a short network failure or selected 5xx response with a small backoff. Do not retry a 401 as if it were transient, and do not create a retry burst after a 429. Cache and request coalescing should remove duplicate traffic first.
  • Keep one typed client registration.
  • Use decimal for rates and amounts.
  • Pass cancellation through the full call chain.
  • Reject non-success HTTP responses before deserialization.
MM
exchangerate.dev
Integration guides for developers building with FX data.

Keep reading

GuideHistorical FX rates and time series in one callRead ReferenceReading source and market_sessionRead GuideIndicative vs executable FX ratesRead