如果你正在 Microsoft 技术栈上构建支付后端、SaaS 计费引擎或企业定价服务,那么在本周交付一个 C# / .NET 汇率 API 集成,将是你能完成的最高价值任务之一。现代 .NET 已经为你准备好了一切——HttpClient、IHttpClientFactory、System.Text.Json、IMemoryCache 以及一流的依赖注入——可是大多数教程仍然只展示一个 WebClient.DownloadString 片段,这种代码根本通不过任何生产环境的代码评审。本文带你从第一个请求开始,一直走到一个强类型、带缓存、具备弹性的客户端,可以放进 ASP.NET Core、Blazor 应用、WPF 桌面工具或 Azure Function。
读完本教程后,你将拥有一个可复用的 Finexly 客户端,可调用 Finexly API 文档,一个小型的控制台货币转换器,以及一个 ASP.NET Core 极简 API,为整个技术栈提供实时汇率——所有代码都遵循 Microsoft 官方推荐的 .NET 8 / .NET 9 惯用模式。
为什么 C# / .NET 是货币集成的强大选择
生产环境中的货币转换主要是个 I/O 问题:获取汇率、缓存它们、做一些小数乘法、返回 JSON。过去十年里 .NET 运行时一直在为这种工作负载做优化:
IHttpClientFactory解决套接字耗尽问题。 它会复用HttpMessageHandler实例,你可以在任何地方注入HttpClient而不必担心套接字泄漏——这是我们在老式 .NET 代码中看到的最大间歇性故障来源。System.Text.Json速度快、分配少。 反序列化一个包含 170 种货币的典型响应远低于 1 毫秒,不需要任何第三方依赖。decimal是语言内置类型。 处理金额时根本不需要第三方BigDecimal——当你对数千行折算后的发票求和时,这一点至关重要。- Polly 是事实上的弹性库。 重试、断路器和超时,只需一次
AddPolicyHandler调用。 - 极简 API 和 AOT。 用 30 行代码搭一个汇率微服务在今天是现实的,并且冷启动只需毫秒。
如果你在评估其他技术栈,我们有平行的指南:Node.js、Python、Go 和 PHP。本文专门针对 C#,并假定你熟悉 async/await、通用主机和依赖注入。
前置条件与项目搭建
你需要安装 .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你还需要一个 Finexly API key。免费注册——无需信用卡,免费套餐每月 1000 次请求,足以支撑开发和 CI。
千万不要把 key 硬编码在代码里。把它存为环境变量:
# 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 key 暴露在 URL 中。我们要逐一修复这些问题。
定义强类型响应模型
System.Text.Json 可以干净地反序列化为 C# 的 record 类型。在 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> 的怪操作。
使用 IHttpClientFactory 构建强类型 HttpClient
.NET 推荐的模式是通过 IHttpClientFactory 注册的强类型客户端。它带来连接池、可配置的 handler 和便捷的 DI。在 Finexly.Client 项目里创建 FinexlyClient.cs:
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 key 放在请求头而不是 URL 里——对日志、代理和链路追踪更安全。
- 5 秒的
Timeout防止一个卡住的汇率 API 把缓慢的结账过程拖死。 - 客户端的每个方法都接受
CancellationToken——这在现代 ASP.NET 中是不可妥协的。 - 它使用
GetFromJsonAsync<T>,在一次低分配的调用中合并了 GET、状态检查和反序列化。
在依赖注入中注册客户端
在你的 Program.cs(控制台、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 注入到任意控制器、极简 API 端点或后台服务中。
用 Polly 添加重试与断路器
外部 API 偶尔会丢请求、做节流,或者在网络层打个嗝。Polly 是 .NET 标准的弹性库,用两行代码就能集成到 IHttpClientFactory。
安装包:
dotnet add Finexly.ConsoleApp package Microsoft.Extensions.Http.Polly配置带回退的重试和断路器:
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 上以指数回退(200ms、400ms、800ms)进行重试,并在 API 完全宕机时触发断路器——这样你的结账页可以优雅降级,而不是把连接挂住几分钟。
用 IMemoryCache 缓存响应
实时汇率很少每分钟变动多于一次,因此一个小的进程内缓存通常能减少 95% 以上的上游调用。.NET 自带 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>();对于结账定价、账单预览和管理后台,60 秒的 TTL 是非常好的默认值。对于历史数据,把 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;构建一个可以丢到任意服务器上的单文件二进制:
dotnet publish Finexly.ConsoleApp -c Release -r linux-x64 \
--self-contained -p:PublishSingleFile=true用 ASP.NET Core 极简 API 对外暴露汇率
对大多数团队而言,正确的架构是一个内部汇率微服务,它包装上游 API、添加缓存,并为其他所有服务提供一个快速、免费、VPC 内的端点。ASP.NET Core 极简 API 把这件事变成了一个 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——那一次舍入误差可能让你输掉一次拒付争议。 - 在展示时取整,而不是在计算时。 在每次乘法中保留完整精度,只有在展示或持久化时才
Math.Round(value, 2, MidpointRounding.ToEven)。 - 固定舍入模式。 银行家舍入(
ToEven)是 IEEE 754 和 ISO 80000-1 中金融行业的默认。 - 把货币当作不透明代码看待。 使用 ISO 4217 字符串——不要假设每种货币都有两位小数。JPY 是零位,BHD 是三位。我们的 ISO 4217 指南涵盖了边界情况。
生产环境最佳实践
还有一些额外模式,能区分玩具项目和你可以维护数年的系统:
- 设置紧凑的 HTTP 超时。 默认的 100 秒
HttpClient.Timeout在高负载下会阻塞线程池。5 秒对汇率 API 完全足够。 - 每个外部调用都包一个
CancellationToken。 ASP.NET Core 免费给你传入一个;尊重它,这样掉线的请求不会继续和上游通信。 - 记录状态码,不记录响应体。 汇率 API 响应过大,不便记录,也可能包含你不想放进 Splunk 的汇率数据。
- 监控你的配额。 比较价格方案,在 CloudWatch / Application Insights 上设置每月上限 80% 的告警。
- 固定你真正使用的货币。 发送
symbols=EUR,GBP,JPY能把响应从 ~10 KB 缩小到 200 字节以下。 - 激进地缓存。 对延迟敏感的结账可以用 30 秒缓存;管理后台可以用 5 分钟。
如果你在评估服务商,我们的免费 vs 付费货币 API 比较涵盖了主流方案在准确性、可用性和价格上的差异。
常见问题
哪种货币 API 最适合 C# 和 .NET?
任何具有干净 JSON 和稳定可用性的 REST API 都能通过 IHttpClientFactory 良好集成。Finexly 专门设计成语言无关——同一组端点被 Node、Python、Go、Java 和 .NET 客户端消费,没有 SDK 绑定。查看我们的免费货币 API 概览了解更多。
应该用 Newtonsoft.Json 还是 System.Text.Json?
对于 .NET 6+ 上的新代码,使用 System.Text.Json。它快 2–3 倍、分配更少、属于基础类库,因此没有额外包要维护。Newtonsoft.Json 仍然很好,值得在遗留代码中保留,但全新的 C# 货币集成应当默认使用 System.Text.Json。
我需要一个 NuGet SDK 还是 HttpClient 就够? 一个手写的强类型客户端(大约 50 行 C# 代码)几乎总是正确答案。它让你完全掌控缓存、重试、遥测和序列化,并避免任何第三方 SDK 的版本漂移风险。本教程中的示例已经可用于生产。
.NET 应用中应多久刷新一次汇率? 对大多数场景——电商结账、SaaS 计费、开票——30 到 60 秒的缓存是安全的。交易系统当然需要实时数据,但其他一切场景,分钟级的新鲜度对用户来说是不可见的,并能大幅降低 API 开销。
我可以在 Azure Functions 或 AWS Lambda 中运行吗?
可以。IHttpClientFactory 在隔离进程的 Azure Functions 和使用 Microsoft.Extensions.Hosting 的 Lambda handler 中工作方式相同。对冷启动敏感的负载,启用 AOT 发布(PublishAot=true),函数将在 100 毫秒内启动。
decimal 与 double 相比慢吗?
decimal 每次算术运算大约比 double 慢 10–20 倍,但一次货币转换最多只做几次乘法。在真实负载中你永远测不出差别——而对金额来说,正确性的提升是不可妥协的。
总结
你现在拥有了一个完整且生产级的 C# / .NET 汇率 API 集成蓝图:强类型模型、由 IHttpClientFactory 支撑的客户端、基于 Polly 的重试和断路器、进程内缓存、控制台转换器,以及 ASP.NET Core 极简 API。这里的每个模式都直接来自 Microsoft 官方指南,并已在真实的金融科技生产代码中得到验证。
准备好把实时汇率交付到你的 .NET 应用了吗?免费获取 Finexly API key——无需信用卡。从每月 1000 次免费请求开始,随着产品增长再升级。如果你想先了解 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 →