在 .NET 微服务中为多个加密货币数据提供商实现策略和适配器模式,使用 GraphQL






4.64/5 (4投票s)
在 .NET 8 微服务中实现策略和适配器模式,以整合多个加密货币数据提供商,并使用 GraphQL 构建动态且可扩展的架构。
引言
在快节奏的加密货币世界中,获取准确的实时数据对于从交易平台到投资组合追踪器等各种应用程序至关重要。然而,由于 API 和数据格式的差异,整合多个加密货币数据提供商会带来挑战。本文将演示如何使用 GraphQL 构建一个可扩展且可扩展的 .NET 8 微服务,整合多个加密货币数据提供商,并利用 **策略** 和 **适配器** 设计模式。我们将专注于创建一个允许轻松添加新提供商而无需修改核心逻辑的架构,确保高性能和可维护性。
背景
该应用程序将是一个 **加密货币微服务**,它将在不同的加密货币数据提供商(例如 CoinGecko、Binance)之间动态切换,并通过 **GraphQL API** 提供一致的数据。它将利用 **策略模式** 来选择不同的数据提供商,并利用 **适配器模式** 将响应标准化为通用格式。
功能要求
-
加密货币数据检索:
- 该应用程序必须从多个外部 API(例如 CoinGecko、Binance)检索加密货币数据(价格和市场数据)。
- 它应该支持查询特定加密货币(例如比特币、以太坊等)的实时价格和市场数据。
-
动态提供商选择:
- 应用程序必须根据用户输入或配置动态地在加密货币数据提供商之间切换。
- 客户端可以在每次 GraphQL 查询时通过指定 `provider` 参数(例如 `CoinGecko` 或 `Binance`)来选择提供商。
-
标准化数据格式:
- 无论数据提供商如何,应用程序都应以一致的格式返回加密货币数据。
- **加密货币价格**:一个字典,其中键是加密货币 ID(例如“bitcoin”、“ethereum”),值是美元价格。
- **市场数据**:一个 `CryptoMarketData` 对象列表,其中包含 `id`、`name`、`currentPrice`、`marketCap`、`totalVolume` 和 `priceChangePercentage24h` 等字段。
- 无论数据提供商如何,应用程序都应以一致的格式返回加密货币数据。
-
GraphQL API:
- 应用程序必须公开一个具有以下查询的 GraphQL API:
- **`cryptoPrices`**:返回一种或多种加密货币的实时价格。
- **`cryptoMarketData`**:返回一种或多种加密货币的详细市场数据。
- 客户端必须能够通过查询中的 `provider` 参数指定要使用的提供商。
- 应用程序必须公开一个具有以下查询的 GraphQL API:
-
错误处理:
- 应用程序必须妥善处理错误,在外部 API 失败或返回无效数据时向客户端提供有意义的错误消息。
- 如果选定的提供商不可用,应用程序应通知客户端并且不会崩溃。
-
提供商可扩展性:
- 系统应易于扩展,以添加新的加密货币数据提供商(例如 Kraken、CoinMarketCap)。
- 添加新提供商应需要对现有系统进行最少的更改,利用策略和适配器模式来插入新提供商。
1. 理解设计模式
为了创建一个灵活且可维护的微服务,整合多个加密货币数据提供商,我们利用了关键的面向对象设计模式。这些模式是 **GoF(四人帮)设计模式** 的一部分,它们是常见软件设计问题的公认解决方案。
GoF 设计模式
“**四人帮**”这个术语指的是 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 这四位作者,他们合著了开创性著作《*设计模式:可复用面向对象软件*》。该书于 1994 年出版,收录了 23 种经典软件设计模式,为面向对象编程中的常见设计问题提供了标准化解决方案。
为何使用 GoF 设计模式?
- **久经考验的解决方案**:GoF 设计模式提供了经过多年软件开发优化和验证的解决方案。
- **可复用性**:它们促进代码复用,减少冗余和出错的可能性。
- **可维护性**:通过提供清晰的结构,模式使代码更易于理解、维护和扩展。
- **灵活性**:策略和适配器等设计模式使系统更能适应变化,以最小的影响来适应新需求。
- **沟通**:它们为开发人员提供了共享的词汇表,从而改善了开发团队内部的协作和理解。
通过利用这些模式,我们解决了整合具有不同接口和数据格式的多个外部 API 所带来的复杂性。具体来说,我们实现了:
策略模式
**策略模式** 是一种行为设计模式,它允许在运行时选择算法的行为。它定义了一系列算法,将每个算法封装起来,并在系列中使其可互换。这种模式允许算法独立于使用它的客户端而变化。
策略模式的关键概念
- **上下文 (Context)**:包含策略引用的对象。
- **策略接口 (Strategy Interface)**:所有支持的算法的通用接口。
- **具体策略 (Concrete Strategies)**:实现具有特定算法的策略接口的类。
在我们的上下文中
- **上下文 (Context)**:我们的微服务,特别是需要加密货币数据的部分。
- **策略接口 (Strategy Interface)**:`ICryptoDataProvider`,定义了 `GetCryptoPricesAsync` 和 `GetCryptoMarketDataAsync` 等方法。
- **具体策略 (Concrete Strategies)**:`CoinGeckoDataProvider` 和 `BinanceDataProvider` 等实现。
通过使用策略模式,我们的应用程序可以根据可用性、性能或用户偏好等因素,在运行时动态地选择使用哪个数据提供商。这意味着,当添加新提供商时,我们应用程序的核心逻辑保持不变;我们只需引入新的具体策略实现。
优点
- **可互换的算法**:在不修改客户端代码的情况下,轻松地在不同算法(数据提供商)之间切换。
- **符合开闭原则**:可以添加新策略,而无需修改现有代码。
- **运行时灵活性**:策略可以在运行时更改,提供动态行为。
我们在应用程序中的示例
假设一个客户端通过我们的 GraphQL API 请求加密货币数据,并指定“Binance”作为提供商。微服务使用工厂检索实现 `ICryptoDataProvider` 的 `BinanceDataProvider`。如果客户端稍后请求来自“CoinGecko”的数据,微服务将检索 `CoinGeckoDataProvider`,而无需更改处理请求的核心逻辑。
适配器模式
**适配器模式** 是一种结构设计模式,它允许具有不兼容接口的对象协同工作。它涉及一个名为适配器的类,该类负责两个独立或不兼容接口之间的通信。
适配器模式的关键概念
- **目标接口 (Target Interface)**:客户端期望的接口。
- **适配器 (Adapter)**:一个实现目标接口并将其调用委托给被适配者的类。
- **被适配者 (Adaptee)**:具有不兼容接口的现有类。
在我们的上下文中
- **目标接口 (Target Interface)**:我们的应用程序期望的标准化格式,由 `AdaptPrices` 和 `AdaptMarketData` 等方法定义。
- **适配器 (Adapters)**:`CoinGeckoAdapter`、`BinanceAdapter`,它们实现了 `ICryptoDataProviderAdapter`。
- **被适配者 (Adaptees)**:CoinGecko 和 Binance 等外部 API 提供的原始数据格式。
通过使用适配器模式,我们可以将来自不同提供商的数据转换为我们的应用程序可以处理的一致格式,而不管提供商的独特数据结构如何。
优点
- **接口兼容性**:允许集成具有不兼容接口的类。
- **可复用性**:使现有功能能够以新的方式使用。
- **符合单一职责原则**:将数据转换逻辑与业务逻辑分离。
我们在应用程序中的示例
当 `BinanceDataProvider` 从 Binance 的 API 获取原始 JSON 数据时,数据格式可能与我们应用程序期望的不符。`BinanceAdapter` 接收此原始数据并将其转换为标准化的 `CryptoMarketData` 对象,使其与我们应用程序的处理逻辑兼容。
2. 架构概述
我们的微服务旨在:
- **整合多个加密货币数据提供商**,如 CoinGecko 和 Binance。
- **在运行时动态切换提供商**,而无需修改核心逻辑。
- **公开 GraphQL API**,允许客户端请求特定提供商的数据。
- **处理错误并实现故障转移机制**以提高健壮性。
- **易于扩展**,以便无缝添加新提供商。
高层架构图
3. 设置 .NET 8 微服务
必备组件
- **Visual Studio 2022** 或更高版本。
- 已安装 **.NET 8 SDK**。
- C#、.NET Core 和 GraphQL 的 **基础知识**。
项目设置
-
**创建新项目**:打开 Visual Studio,创建一个新的 **ASP.NET Core Web API** 项目,命名为 `CryptoMicroservice`,目标为 **.NET 8.0**。
-
**禁用控制器**:取消选中 **使用控制器** 选项,因为我们将使用 GraphQL。
-
添加 NuGet 包:
Install-Package HotChocolate.AspNetCore Install-Package System.Text.Json
-
创建项目结构文件夹:
- 适配器
- 模型
- 接口
- GraphQL
- 工厂
- DataProviders
4. 实现策略模式
定义策略接口
策略接口定义了所有具体策略必须实现的方法。在我们的例子中,`ICryptoDataProvider` 作为策略接口。
// Interfaces/ICryptoDataProvider.cs
public interface ICryptoDataProvider
{
string ProviderName { get; }
Task<Dictionary<string, decimal>> GetCryptoPricesAsync(string[] cryptoIds);
Task<List<CryptoMarketData>> GetCryptoMarketDataAsync(string[] cryptoIds);
}
解释
- **`ProviderName`**:标识提供商,允许工厂选择正确的策略。
- **`GetCryptoPricesAsync`**:检索指定加密货币的当前价格。
- **`GetCryptoMarketDataAsync`**:检索详细的市场数据。
创建提供商工厂
工厂负责根据某些标准(例如提供商的名称)选择合适的策略(数据提供商)。
// Interfaces/ICryptoDataProviderFactory.cs
public interface ICryptoDataProviderFactory
{
ICryptoDataProvider GetProvider(string providerName);
}
// Factories/CryptoDataProviderFactory.cs public class CryptoDataProviderFactory : ICryptoDataProviderFactory { private readonly IDictionary<string, ICryptoDataProvider> _providers; public CryptoDataProviderFactory(IEnumerable<ICryptoDataProvider> providers) { _providers = providers.ToDictionary(p => p.ProviderName, StringComparer.OrdinalIgnoreCase); } public ICryptoDataProvider GetProvider(string providerName) { if (_providers.TryGetValue(providerName, out var provider)) return provider; throw new ArgumentException($"Provider '{providerName}' not found."); } }
解释
- 工厂维护一个可用提供商的集合。
- 它公开一个按名称检索提供商的方法。
- 它根据输入提供商(例如 ConGeckoDataProvider、BinanceDataProvider 等)返回 DataProvider。
- 这使得应用程序可以在运行时动态地在提供商之间切换。
在中间层注册工厂
要在 `Program.cs` 中注册工厂,请使用以下代码:
// program.cs
// Register the Factory
builder.Services.AddSingleton<ICryptoDataProviderFactory, CryptoDataProviderFactory>();
5. 实现适配器模式
定义适配器接口
适配器接口定义了将数据从提供商格式转换为我们标准化格式所需的方法。
// Interfaces/ICryptoDataProviderAdapter.cs
public interface ICryptoDataProviderAdapter
{
Dictionary<string, decimal> AdaptPrices(string rawData);
List<CryptoMarketData> AdaptMarketData(string rawData);
}
解释
- **`AdaptPrices`**:将原始价格数据转换为标准化的字典。
- **`AdaptMarketData`**:将原始市场数据转换为 `CryptoMarketData` 对象列表。
为每个提供商实现适配器
CoinGecko 适配器
// Adapters/CoinGeckoAdapter.cs
using System.Text.Json;
public class CoinGeckoAdapter : ICryptoDataProviderAdapter
{
public Dictionary<string, decimal> AdaptPrices(string rawData)
{
using var jsonDocument = JsonDocument.Parse(rawData);
var root = jsonDocument.RootElement;
var prices = new Dictionary<string, decimal>();
foreach (var property in root.EnumerateObject())
{
var cryptoId = property.Name;
var usdPrice = property.Value.GetProperty("usd").GetDecimal();
prices.Add(cryptoId, usdPrice);
}
return prices;
}
public List<CryptoMarketData> AdaptMarketData(string rawData)
{
using var jsonDocument = JsonDocument.Parse(rawData);
var root = jsonDocument.RootElement;
var marketDataList = new List<CryptoMarketData>();
foreach (var item in root.EnumerateArray())
{
var marketData = new CryptoMarketData
{
Id = item.GetProperty("id").GetString(),
Symbol = item.GetProperty("symbol").GetString(),
Name = item.GetProperty("name").GetString(),
CurrentPrice = item.GetProperty("current_price").GetDecimal(),
MarketCap = item.GetProperty("market_cap").GetDecimal(),
Volume = item.GetProperty("total_volume").GetDecimal()
};
marketDataList.Add(marketData);
}
return marketDataList;
}
}
Binance 适配器
// Adapters/BinanceAdapter.cs public class BinanceAdapter : ICryptoDataProviderAdapter { public Dictionary<string, decimal> AdaptPrices(string rawData) { using var jsonDocument = JsonDocument.Parse(rawData); var root = jsonDocument.RootElement; var prices = new Dictionary<string, decimal>(); foreach (var item in root.EnumerateArray()) { var symbol = item.GetProperty("symbol").GetString().ToLower(); var price = Convert.ToDecimal( item.GetProperty("price").GetString()); prices.Add(symbol, price); } return prices; } public List<CryptoMarketData> AdaptMarketData(string rawData) { var marketDataList = new List<CryptoMarketData>(); using var jsonDocument = JsonDocument.Parse(rawData); var root = jsonDocument.RootElement; // The response is a single JSON object var marketData = new CryptoMarketData { Id = root.GetProperty("symbol").GetString(), // Binance does not provide an ID Symbol = root.GetProperty("symbol").GetString(), Name = BinanceHelper.Instance.GetCryptoNameFromSymbol(root.GetProperty("symbol").GetString()), CurrentPrice = Convert.ToDecimal( root.GetProperty("lastPrice").GetString()), MarketCap = 0, // Binance's endpoint does not provide market cap Volume = Convert.ToDecimal(root.GetProperty("volume").GetString()) }; marketDataList.Add(marketData); return marketDataList; } }
解释
- 每个适配器都实现了将原始数据转换为我们标准化格式的方法。
- 它们处理特定于提供商的数据结构和细微差别。
- 这确保了我们应用程序的其余部分可以处理一致的数据模型。
- BinanceHelper 已在此 处 引入。
6. 集成外部加密货币 API
使用 `IHttpClientFactory`
在 Program.cs 中为每个提供商注册命名的 `HttpClient` 实例。在我们的例子中,我们为 CoinGecko 和 Binance 注册。
// Program.cs
builder.Services.AddHttpClient("CoinGecko", client =>
{
client.BaseAddress = new Uri("https://api.coingecko.com/api/v3/"); // Base URL
});
builder.Services.AddHttpClient("Binance", client =>
{
client.BaseAddress = new Uri("https://api.binance.com/"); // Base URL
});
实现数据提供商
CoinGecko 数据提供商
// DataProviders/CoinGeckoDataProvider.cs
public class CoinGeckoDataProvider : ICryptoDataProvider
{
public string ProviderName => "CoinGecko";
private readonly HttpClient _httpClient;
private readonly ICryptoDataProviderAdapter _adapter;
public CoinGeckoDataProvider(IHttpClientFactory httpClientFactory, ICryptoDataProviderAdapter adapter)
{
_httpClient = httpClientFactory.CreateClient("CoinGecko");
_adapter = adapter;
}
public async Task<Dictionary<string, decimal>> GetCryptoPricesAsync(string[] cryptoIds)
{
var url = $"simple/price?ids={string.Join(",", cryptoIds)}&vs_currencies=usd";
var response = await _httpClient.GetStringAsync(url);
return _adapter.AdaptPrices(response);
}
public async Task<List<CryptoMarketData>> GetCryptoMarketDataAsync(string[] cryptoIds)
{
var url = $"coins/markets?vs_currency=usd&ids={string.Join(",", cryptoIds)}";
var response = await _httpClient.GetStringAsync(url);
return _adapter.AdaptMarketData(response);
}
}
Binance 数据提供商(带符号映射和优化)
// DataProviders/BinanceDataProvider.cs
public class BinanceDataProvider : ICryptoDataProvider
{
public string ProviderName => "Binance";
private readonly HttpClient _httpClient;
private readonly ICryptoDataProviderAdapter _adapter;
public BinanceDataProvider(IHttpClientFactory httpClientFactory, ICryptoDataProviderAdapter adapter)
{
_httpClient = httpClientFactory.CreateClient("Binance");
_adapter = adapter;
}
public async Task<Dictionary<string, decimal>> GetCryptoPricesAsync(string[] cryptoIds)
{
// Map cryptoIds to Binance symbols
var binanceSymbols = cryptoIds.Select(id =>Adapters.BinanceHelper.Instance.GetBinanceSymbol(id)).ToList();
// Fetch all prices from Binance API
var response = await _httpClient.GetStringAsync("api/v3/ticker/price");
// Parse the JSON response
using var jsonDocument = JsonDocument.Parse(response);
var root = jsonDocument.RootElement;
// Filter the JSON elements to include only the requested binanceSymbols
var filteredElements = root.EnumerateArray()
.Where(item =>
{
var symbol = item.GetProperty("symbol").GetString().ToLower();
return binanceSymbols.Contains(symbol, StringComparer.OrdinalIgnoreCase);
})
.ToList();
// Serialize the filtered elements back to JSON string
var filteredResponse = JsonSerializer.Serialize(filteredElements);
// Pass the filtered JSON to the adapter
var prices = _adapter.AdaptPrices(filteredResponse);
return prices;
}
public async Task<List<CryptoMarketData>> GetCryptoMarketDataAsync(string[] cryptoIds)
{
// Binance does not accept multiple symbols in a single request for market data
var tasks = new List<Task<CryptoMarketData>>();
foreach (var cryptoId in cryptoIds)
{
tasks.Add(GetMarketDataForSymbolAsync(cryptoId));
}
// Wait for all tasks to complete
var marketDataArray = await Task.WhenAll(tasks);
return marketDataArray.ToList();
}
private async Task<CryptoMarketData> GetMarketDataForSymbolAsync(string cryptoId)
{
var symbol = Adapters.BinanceHelper.Instance.GetBinanceSymbol(cryptoId);
var url = $"api/v3/ticker/24hr?symbol={symbol}";
var response = await _httpClient.GetStringAsync(url);
// Use the adapter to adapt the raw market data
var marketData = _adapter.AdaptMarketData(response);
// Since AdaptMarketData returns a list, but we only have one item, get the first item
return marketData.FirstOrDefault();
}
}
此处,**`BinanceHelper`** 是一个惰性单例,用于处理符号映射。它也由 `BinanceAdapter.cs` 使用。
// Adapters/BinanceHelper.cs
public class BinanceHelper
{
// Lazy initialization of the singleton instance
private static readonly Lazy<BinanceHelper> _instance = new Lazy<BinanceHelper>(() => new BinanceHelper());
// Private constructor to prevent instantiation from outside
private BinanceHelper() { }
// Public accessor for the singleton instance
public static BinanceHelper Instance => _instance.Value;
public string GetCryptoNameFromSymbol(string symbol)
{
return symbol switch
{
"BTCUSDT" => "Bitcoin",
"ETHUSDT" => "Ethereum",
"XRPUSDT" => "Ripple",
"LTCUSDT" => "Litecoin",
// Add more mappings as needed
_ => "Unknown"
};
}
public string GetBinanceSymbol(string cryptoId)
{
return cryptoId.ToLower() switch
{
"bitcoin" => "BTCUSDT",
"ethereum" => "ETHUSDT",
"ripple" => "XRPUSDT",
"litecoin" => "LTCUSDT",
// Add more mappings as needed
_ => throw new ArgumentException($"Unsupported cryptoId: {cryptoId}")
};
}
}
解释
- **符号映射**:Binance 使用交易对符号(例如“BTCUSDT”)而不是标准加密货币 ID。
- **适配前过滤**:我们在将原始数据传递给适配器之前对其进行过滤,以优化性能。
- **市场数据检索中的并发**:由于 Binance 的 API 要求为每个符号单独请求市场数据,因此我们并发地获取数据以提高性能。
在中间层注册提供商
为了将 CoinGecko 和 Binance 提供商集成到应用程序中,您需要将它们各自的 `HttpClient`、适配器和数据提供商注册到 `Program.cs`。您可以这样做:
// program.cs
// Register CoinGecko Data Provider
builder.Services.AddSingleton<ICryptoDataProvider>(serviceProvider =>
{
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var adapter = serviceProvider.GetServices<ICryptoDataProviderAdapter>().First(a => a is CoinGeckoAdapter);
return new CoinGeckoDataProvider(httpClientFactory, adapter);
});
// Register Binance Data Provider
builder.Services.AddSingleton<ICryptoDataProvider>(serviceProvider =>
{
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var adapter = serviceProvider.GetServices<ICryptoDataProviderAdapter>().First(a => a is BinanceAdapter);
return new BinanceDataProvider(httpClientFactory, adapter);
});
7. 构建 GraphQL API
设置 Hot Chocolate
安装 Hot Chocolate 包
Install-Package HotChocolate.AspNetCore
定义 GraphQL 类型
// GraphQL/CryptoMarketDataType.cs
using HotChocolate.Types;
public class CryptoMarketDataType : ObjectType<CryptoMarketData>
{
protected override void Configure(IObjectTypeDescriptor<CryptoMarketData> descriptor)
{
descriptor.Field(f => f.Id).Type<StringType>();
descriptor.Field(f => f.Name).Type<StringType>();
descriptor.Field(f => f.Symbol).Type<StringType>();
descriptor.Field(f => f.CurrentPrice).Type<DecimalType>();
descriptor.Field(f => f.MarketCap).Type<DecimalType>();
descriptor.Field(f => f.Volume).Type<DecimalType>();
}
}
实现 GraphQL 查询
// GraphQL/Query.cs public class Query { private readonly ICryptoDataProviderFactory _providerFactory; public Query(ICryptoDataProviderFactory providerFactory) { _providerFactory = providerFactory; } [GraphQLName("cryptoPrices")] public async Task<Dictionary<string, decimal>> GetCryptoPrices(string[] cryptoIds, string provider) { var dataProvider = _providerFactory.GetProvider(provider); return await dataProvider.GetCryptoPricesAsync(cryptoIds); } [GraphQLName("cryptoMarketData")] public async Task<List<CryptoMarketData>> GetCryptoMarketData(string[] cryptoIds, string provider) { var dataProvider = _providerFactory.GetProvider(provider); return await dataProvider.GetCryptoMarketDataAsync(cryptoIds); } }
配置 GraphQL 服务器
// Program.cs
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddType<CryptoMarketDataType>();
8. 错误处理和故障转移机制
实现错误处理
在数据提供商中添加异常处理,以优雅地捕获和处理 API 错误。
public async Task<Dictionary<string, decimal>> GetCryptoPricesAsync(string[] cryptoIds)
{
try
{
// Existing implementation
}
catch (HttpRequestException ex)
{
// Log the error
throw new Exception("Failed to fetch data from the provider.", ex);
}
}
实现故障转移
修改工厂,在请求的提供商失败时提供默认提供商。
public ICryptoDataProvider GetProvider(string providerName)
{
if (_providers.TryGetValue(providerName, out var provider))
return provider;
// Return a default provider or handle the error
throw new ArgumentException($"Provider '{providerName}' not found.");
}
9. 使用 Banana Cake Pop 进行测试
设置 Banana Cake Pop
从 Hot Chocolate 12 版本开始,**Banana Cake Pop** 作为内置中间件包含在内,供开发人员测试和交互他们的 GraphQL API。默认情况下,当您使用 `MapGraphQL()` 映射 GraphQL 端点时,Banana Cake Pop 会自动在 `/graphql` 端点提供服务。但是,在我们的配置中,我们将 Banana Cake Pop 映射到 `/graphql-ui`,以将开发 IDE 与生产 API 端点分开。
这种方法提供了一个专用的界面来测试和探索我们的 GraphQL schema,而不会干扰 API 的主要 `/graphql` 端点,从而提高了安全性和清晰度。
- 安装 Banana Cake Pop 包
Install-Package HotChocolate.AspNetCore.BananaCakePop
- 在 Program.cs 中配置 Banana Cake Pop
// Program.cs
var app = builder.Build();
// Enable GraphQL middleware
app.MapGraphQL("/graphql");
// Enable Banana Cake Pop middleware
if (app.Environment.IsDevelopment())
{
app.MapBananaCakePop("/graphql-ui");
}
app.Run();
解释
- **`app.MapGraphQL("/graphql")`**:将 GraphQL API 端点映射到 `/graphql`,客户端应用程序将在此处发送其查询和突变。
- **`app.MapBananaCakePop("/graphql-ui")**:将 Banana Cake Pop GraphQL IDE 映射到 `/graphql-ui`。这为开发人员提供了一个用户友好的界面来测试和调试 GraphQL 查询。
- **条件中间件注册**:我们将 Banana Cake Pop 映射包装在一个 `if` 语句中,该语句检查应用程序是否在开发环境中运行。这确保了 IDE 仅在开发期间可用,而在生产环境中不可用,从而提高了安全性。
为何将 Banana Cake Pop 映射到 `/graphql-ui`?
默认情况下,Banana Cake Pop 将在与 GraphQL API (/graphql
) 相同的端点可用。将 IDE 与 API 端点分开有几个好处:
- **安全性**:在生产环境中公开开发工具可能存在安全风险。通过将 Banana Cake Pop 映射到不同的端点并将其限制在开发环境中,我们可以降低此风险。
- **清晰性**:它区分了客户端应用程序使用的 API 端点和开发人员使用的开发工具。
- **灵活性**:允许自定义配置和更轻松的访问管理,例如对 IDE 应用不同的身份验证或授权策略。
访问 Banana Cake Pop
运行应用程序后,您可以通过导航到以下地址来访问 Banana Cake Pop IDE:
<code>https://:{port}/graphql-ui</code>
将 `{port}` 替换为应用程序正在运行的实际端口号。
修改启动配置文件以自动打开 Banana Cake Pop
除了将 Banana Cake Pop 映射到自定义端点之外,您还可以配置开发环境,以便在启动应用程序时自动启动 Banana Cake Pop IDE。这对于开发和测试非常方便。
在 Visual Studio 中更改启动配置文件
要设置应用程序以便在启动时自动打开 `/graphql-ui` 端点,您可以修改 Visual Studio 中的启动设置:
-
找到 `launchSettings.json`:
- 在您的项目中,导航到 `Properties` 文件夹。
- 打开 `launchSettings.json` 文件。
-
修改启动 URL:
- 找到您正在使用的启动配置文件,通常在 `"profiles"` 下。
- 添加或修改 `"launchUrl"` 属性以包含 `graphql-ui`。
示例
{ "profiles": { "CryptoMicroservice": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "graphql-ui", "applicationUrl": "https://:5001;https://:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
- 解释:
- **`"launchUrl": "graphql-ui"`**:设置应用程序启动时浏览器将导航到的 URL 路径。
- **`"launchBrowser": true`**:确保浏览器自动启动。
-
保存更改:
- 修改 `launchSettings.json` 后,保存文件。
-
重启应用程序:
- 运行您的应用程序(例如,在 Visual Studio 中按 **F5**)。
- 浏览器现在应自动在 `https://:{port}/graphql-ui` 打开。
修改启动配置文件的优点
- **便捷性**:在启动应用程序时自动打开 Banana Cake Pop IDE,节省开发时间。
- **效率**:通过立即提供对 GraphQL IDE 的访问,简化了测试和调试过程。
- **专注**:让您可以专注于开发和测试您的 GraphQL 查询,而无需手动导航到 IDE。
重要说明
- **仅限开发环境**:确保此配置仅在开发环境中处于活动状态,以避免在生产环境中出现意外行为。
- **多个配置文件**:如果您有多个启动配置文件(例如,用于不同的环境或配置),请确保更新适当的配置文件。
备选方案:通过 Visual Studio UI 修改调试属性
如果您更喜欢使用 Visual Studio 界面:
-
右键单击项目:
- 在 **解决方案资源管理器** 中,右键单击您的项目(例如 `CryptoMicroservice`)。
- 选择 **属性**。
-
导航到调试选项卡:
- 在项目属性窗口中,单击 **调试** 选项卡。
-
设置启动 URL:
- 在 **启动浏览器** 部分,将 **启动 URL** 设置为 `graphql-ui`。
-
保存并运行:
- 保存您的更改。
- 运行应用程序,浏览器应打开到 Banana Cake Pop IDE。
通过修改启动配置文件以包含 `graphql-ui`,您可以增强开发工作流程,使其能够更快、更轻松地使用 Banana Cake Pop 测试和调试您的 GraphQL API。
开发中的示例用法
有了这个设置,开发人员可以:
- **探索 Schema**:查看 GraphQL API 中定义的所有可用查询、突变和类型。
- **测试查询和突变**:编写并执行 GraphQL 操作以测试 API 的功能。
- **检查响应**:检查 API 返回的数据以及任何错误消息或堆栈跟踪(如果启用了异常详细信息)。
重要说明
- **生产环境**:在生产环境中,Banana Cake Pop 将不可用,因为 `IsDevelopment()` 检查将失败。这确保了最终用户和潜在攻击者无法访问开发工具。
- **自定义**:您可以将 Banana Cake Pop 的路径或其他设置自定义为通过向 `MapBananaCakePop()` 方法提供选项。
测试 API
示例查询:获取加密货币价格
query {
cryptoPrices(cryptoIds: ["bitcoin", "ethereum"], provider: "CoinGecko") {
bitcoin
ethereum
}
}
示例查询:获取市场数据
query {
cryptoMarketData(cryptoIds: ["bitcoin"], provider: "Binance") {
symbol
name
currentPrice
volume
}
}
10. 使用新提供商扩展微服务
添加新提供商(例如 **Kraken**)涉及:
-
实现适配器:
// Adapters/KrakenAdapter.cs public class KrakenAdapter : ICryptoDataProviderAdapter { public Dictionary<string, decimal> AdaptPrices(string rawData) { // Adapt Kraken's price data } public List<CryptoMarketData> AdaptMarketData(string rawData) { // Adapt Kraken's market data } }
-
实现数据提供商:
// DataProviders/KrakenDataProvider.cs public class KrakenDataProvider : ICryptoDataProvider { public string ProviderName => "Kraken"; private readonly HttpClient _httpClient; private readonly ICryptoDataProviderAdapter _adapter; public KrakenDataProvider(IHttpClientFactory httpClientFactory, ICryptoDataProviderAdapter adapter) { _httpClient = httpClientFactory.CreateClient("Kraken"); _adapter = adapter; } public async Task<Dictionary<string, decimal>> GetCryptoPricesAsync(string[] cryptoIds) { // Implement using Kraken's API } public async Task<List<CryptoMarketData>> GetCryptoMarketDataAsync(string[] cryptoIds) { // Implement using Kraken's API } }
-
注册提供商:
// Program.cs builder.Services.AddHttpClient("Kraken", client => { client.BaseAddress = new Uri("https://api.kraken.com/"); }); builder.Services.AddSingleton<ICryptoDataProviderAdapter, KrakenAdapter>(); builder.Services.AddSingleton<ICryptoDataProvider>(serviceProvider => { var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>(); var adapter = serviceProvider.GetServices<ICryptoDataProviderAdapter>().First(a => a is KrakenAdapter); return new KrakenDataProvider(httpClientFactory, adapter); });
无需更改工厂或 GraphQL 查询类。
11. 结论
通过利用 **策略** 和 **适配器** 设计模式,以及提供商 **工厂**,我们构建了一个灵活且可扩展的 .NET 8 微服务,该服务使用 GraphQL 集成了多个加密货币数据提供商。该架构符合 SOLID 设计的 **开闭原则**,允许在不修改现有代码的情况下添加新提供商。通过优化数据处理并处理特定于提供商的细微差别,我们确保了高绩效和可靠性。使用 `System.Text.Json` 可提高性能并减少依赖性。
总结
- **策略模式**:允许在运行时动态选择数据提供商,使系统灵活且易于扩展。
- **适配器模式**:标准化来自不同提供商的数据格式,允许应用程序统一处理数据。
- **工厂模式**:管理数据提供商实例的创建和检索,使客户端代码与具体实现分离。
- **GraphQL API**:为客户端提供强大灵活的接口来请求特定提供商的数据。
12. 参考资料
附录
模型
CryptoMarketData 数据模型
// Models/CryptoMarketData.cs
public class CryptoMarketData
{
public string Id { get; set; }
public string Symbol { get; set; }
public string Name { get; set; }
public decimal CurrentPrice { get; set; }
public decimal MarketCap { get; set; }
public decimal Volume { get; set; }
}
完整的 Program.cs
using CryptoMicroservice.Adapters;
using CryptoMicroservice.DataProviders;
using CryptoMicroservice.Factories;
using CryptoMicroservice.GraphQL;
using CryptoMicroservice.Interfaces;
var builder = WebApplication.CreateBuilder(args);
// Register HttpClient instances
builder.Services.AddHttpClient("CoinGecko", client =>
{
client.BaseAddress = new Uri("https://api.coingecko.com/api/v3/");
});
builder.Services.AddHttpClient("Binance", client =>
{
client.BaseAddress = new Uri("https://api.binance.com/");
});
// Register Adapters
builder.Services.AddSingleton<ICryptoDataProviderAdapter, CoinGeckoAdapter>();
builder.Services.AddSingleton<ICryptoDataProviderAdapter, BinanceAdapter>();
// Register Data Providers
builder.Services.AddSingleton<ICryptoDataProvider>(serviceProvider =>
{
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var adapter = serviceProvider.GetServices<ICryptoDataProviderAdapter>().First(a => a is CoinGeckoAdapter);
return new CoinGeckoDataProvider(httpClientFactory, adapter);
});
builder.Services.AddSingleton<ICryptoDataProvider>(serviceProvider =>
{
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var adapter = serviceProvider.GetServices<ICryptoDataProviderAdapter>().First(a => a is BinanceAdapter);
return new BinanceDataProvider(httpClientFactory, adapter);
});
// Register the Factory
builder.Services.AddSingleton<ICryptoDataProviderFactory, CryptoDataProviderFactory>();
// Register GraphQL services
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddType<CryptoMarketDataType>();
var app = builder.Build();
// Enable GraphQL middleware
app.MapGraphQL("/graphql");
// Enable Banana Cake Pop middleware
if (app.Environment.IsDevelopment())
{
app.MapBananaCakePop("/graphql-ui");
}
app.Run();
**祝编码愉快!** 如果您有任何问题或需要进一步的帮助,请随时联系。