This article demonstrates how to build a scalable and extensible .NET 8 microservice that integrates multiple cryptocurrency data providers using the Strategy and Adapter design patterns, and exposes a GraphQL API for dynamic provider selection. By leveraging these patterns, we create an architecture that allows for seamless addition of new providers without modifying existing code, ensuring high performance and maintainability. The microservice handles provider-specific nuances, optimizes data processing, and provides real-time cryptocurrency data, making it ideal for applications like trading platforms and portfolio trackers.
Introduction
In the fast-paced world of cryptocurrency, accessing accurate and real-time data is crucial for applications ranging from trading platforms to portfolio trackers. However, integrating multiple cryptocurrency data providers presents challenges due to varying APIs and data formats. This article demonstrates how to build a scalable and extensible .NET 8 microservice that integrates multiple cryptocurrency data providers using GraphQL, leveraging the Strategy and Adapter design patterns. We'll focus on creating an architecture that allows for easy addition of new providers without modifying the core logic, ensuring high performance and maintainability.
Github Repository
Background
The application will be a cryptocurrency microservice that dynamically switches between different cryptocurrency data providers (e.g., CoinGecko, Binance) and serves consistent data through a GraphQL API. It will utilize the Strategy Pattern to select different data providers and the Adapter Pattern to standardize the responses into a common format.
Functional Requirements:
-
Cryptocurrency Data Retrieval:
- The application must retrieve cryptocurrency data (prices and market data) from multiple external APIs (e.g., CoinGecko, Binance).
- It should support querying real-time prices and market data for specific cryptocurrencies, such as Bitcoin, Ethereum, etc.
-
Dynamic Provider Selection:
- The application must dynamically switch between cryptocurrency data providers based on user input or configuration.
- The provider can be selected by the client during each GraphQL query by specifying a
provider
argument (e.g., CoinGecko
or Binance
).
-
Standardized Data Format:
- Regardless of the data provider, the application should return cryptocurrency data in a consistent format:
- Crypto Prices: A dictionary with keys being the cryptocurrency IDs (e.g., "bitcoin", "ethereum") and values being the prices in USD.
- Market Data: A list of
CryptoMarketData
objects with fields such as id
, name
, currentPrice
, marketCap
, totalVolume
, and priceChangePercentage24h
.
-
GraphQL API:
- The application must expose a GraphQL API with the following queries:
cryptoPrices
: Returns real-time prices for one or more cryptocurrencies. cryptoMarketData
: Returns detailed market data for one or more cryptocurrencies.
- The client must be able to specify which provider to use via the
provider
argument in the query.
-
Error Handling:
- The application must handle errors gracefully, providing meaningful error messages to the client if an external API fails or returns invalid data.
- If the selected provider is unavailable, the application should notify the client and not crash.
-
Provider Extensibility:
- The system should be easily extensible to add new cryptocurrency data providers (e.g., Kraken, CoinMarketCap).
- Adding a new provider should require minimal changes to the existing system, leveraging the Strategy and Adapter Patterns to plug in new providers.
1. Understanding the Design Patterns
To create a flexible and maintainable microservice that integrates multiple cryptocurrency data providers, we utilize key object-oriented design patterns. These patterns are part of the GoF (Gang of Four) design patterns, which are widely recognized solutions to common software design problems.
GoF Design Patterns
The term Gang of Four refers to the four authors—Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides—of the seminal book "Design Patterns: Elements of Reusable Object-Oriented Software". Published in 1994, this book catalogs 23 classic software design patterns that provide standardized solutions to common design issues in object-oriented programming.
Why Use GoF Design Patterns?
- Proven Solutions: GoF design patterns offer time-tested solutions that have been refined over years of software development.
- Reusability: They promote code reuse, reducing redundancy and the potential for errors.
- Maintainability: By providing clear structures, patterns make code easier to understand, maintain, and extend.
- Flexibility: Design patterns like Strategy and Adapter enable systems to be more adaptable to change, accommodating new requirements with minimal impact on existing code.
- Communication: They provide a shared vocabulary for developers, improving collaboration and understanding within development teams.
By leveraging these patterns, we address the complexities of integrating multiple external APIs with varying interfaces and data formats. Specifically, we implement:
Strategy Pattern
The Strategy Pattern is a behavioral design pattern that enables selecting an algorithm's behavior at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable within that family. This pattern allows the algorithm to vary independently from clients that use it.
Key Concepts of the Strategy Pattern:
- Context: The object that contains a reference to a strategy.
- Strategy Interface: An interface common to all supported algorithms.
- Concrete Strategies: Classes that implement the strategy interface with specific algorithms.
In Our Context:
- Context: Our microservice, specifically the part that requires cryptocurrency data.
- Strategy Interface:
ICryptoDataProvider
, defining methods like GetCryptoPricesAsync
and GetCryptoMarketDataAsync
. - Concrete Strategies: Implementations like
CoinGeckoDataProvider
and BinanceDataProvider
.
By using the Strategy Pattern, our application can dynamically select which data provider to use at runtime based on factors like availability, performance, or user preference. This means that the core logic of our application remains unchanged when adding new providers; we simply introduce new concrete strategy implementations.
Advantages:
- Interchangeable Algorithms: Easily switch between different algorithms (data providers) without altering the client code.
- Open/Closed Principle Compliance: New strategies can be added without modifying existing code.
- Runtime Flexibility: The strategy can be changed at runtime, offering dynamic behavior.
Example in Our Application:
Imagine a client requests cryptocurrency data through our GraphQL API and specifies "Binance" as the provider. The microservice uses the factory to retrieve the BinanceDataProvider
, which implements ICryptoDataProvider
. If the client later requests data from "CoinGecko," the microservice retrieves the CoinGeckoDataProvider
without any changes to the core logic handling the request.
Adapter Pattern
The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It involves a single class called an adapter, which is responsible for communication between two independent or incompatible interfaces.
Key Concepts of the Adapter Pattern:
- Target Interface: The interface expected by the client.
- Adapter: A class that implements the target interface and translates calls to the adaptee.
- Adaptee: The existing class with an incompatible interface.
In Our Context:
- Target Interface: The standardized format our application expects, defined by methods like
AdaptPrices
and AdaptMarketData
. - Adapters:
CoinGeckoAdapter
, BinanceAdapter
, which implement ICryptoDataProviderAdapter
. - Adaptees: The raw data formats provided by external APIs like CoinGecko and Binance.
By using the Adapter Pattern, we can convert the data from various providers into a consistent format that our application can process, regardless of the provider's unique data structure.
Advantages:
- Interface Compatibility: Allows integration of classes with incompatible interfaces.
- Reusability: Enables existing functionality to be used in new ways.
- Single Responsibility Principle Compliance: Separates data conversion logic from business logic.
Example in Our Application:
When BinanceDataProvider
fetches raw JSON data from Binance's API, the data format may not match what our application expects. The BinanceAdapter
takes this raw data and transforms it into standardized CryptoMarketData
objects, making it compatible with our application's processing logic.
2. Architectural Overview
Our microservice aims to:
- Integrate multiple cryptocurrency data providers like CoinGecko and Binance.
- Dynamically switch between providers at runtime without modifying core logic.
- Expose a GraphQL API that allows clients to request data from specific providers.
- Handle errors and implement failover mechanisms for robustness.
- Be easily extensible to add new providers seamlessly.
High-Level Architecture Diagram:
3. Setting Up the .NET 8 Microservice
Prerequisites
- Visual Studio 2022 or later.
- .NET 8 SDK installed.
- Basic knowledge of C#, .NET Core, and GraphQL.
Project Setup
-
Create a New Project: Open Visual Studio and create a new ASP.NET Core Web API project named CryptoMicroservice
targeting .NET 8.0.
-
Disable Controllers: Uncheck the Use controllers option, as we'll be using GraphQL.
-
Add NuGet Packages:
Install-Package HotChocolate.AspNetCore
Install-Package System.Text.Json
-
Create Project Structure Folders:
- Adapters
- Models
- Interfaces
- GraphQL
- Factories
- DataProviders
4. Implementing the Strategy Pattern
Defining the Strategy Interface
The strategy interface defines the methods that all concrete strategies must implement. In our case, ICryptoDataProvider
serves as the strategy interface.
public interface ICryptoDataProvider
{
string ProviderName { get; }
Task<Dictionary<string, decimal>> GetCryptoPricesAsync(string[] cryptoIds);
Task<List<CryptoMarketData>> GetCryptoMarketDataAsync(string[] cryptoIds);
}
Explanation:
ProviderName
: Identifies the provider, allowing the factory to select the correct strategy. GetCryptoPricesAsync
: Retrieves current prices for specified cryptocurrencies. GetCryptoMarketDataAsync
: Retrieves detailed market data.
Creating the Provider Factory
The factory is responsible for selecting the appropriate strategy (data provider) based on some criteria, such as the provider's name.
public interface ICryptoDataProviderFactory
{
ICryptoDataProvider GetProvider(string providerName);
}
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.");
}
}
Explanation:
- The factory maintains a collection of available providers.
- It exposes a method to retrieve a provider by name.
- It returns the DataProvider based on the input provider (e.g. ConGeckoDataProvider, BinanceDataProvider, etc)
- This allows the application to switch between providers dynamically at runtime.
Register the factory in the Middle Layer
To register the factory in Program.cs
, use the following code:
builder.Services.AddSingleton<ICryptoDataProviderFactory, CryptoDataProviderFactory>();
5. Implementing the Adapter Pattern
Defining the Adapter Interface
The adapter interface defines the methods required to convert data from the provider's format to our standardized format.
public interface ICryptoDataProviderAdapter
{
Dictionary<string, decimal> AdaptPrices(string rawData);
List<CryptoMarketData> AdaptMarketData(string rawData);
}
Explanation:
AdaptPrices
: Converts raw price data into a standardized dictionary. AdaptMarketData
: Converts raw market data into a list of CryptoMarketData
objects.
Implementing Adapters for Each Provider
CoinGecko Adapter
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 Adapter
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;
var marketData = new CryptoMarketData
{
Id = root.GetProperty("symbol").GetString(),
Symbol = root.GetProperty("symbol").GetString(),
Name = BinanceHelper.Instance.GetCryptoNameFromSymbol(root.GetProperty("symbol").GetString()),
CurrentPrice = Convert.ToDecimal( root.GetProperty("lastPrice").GetString()),
MarketCap = 0,
Volume = Convert.ToDecimal(root.GetProperty("volume").GetString())
};
marketDataList.Add(marketData);
return marketDataList;
}
}
Explanation:
- Each adapter implements the methods to convert raw data into our standardized format.
- They handle provider-specific data structures and nuances.
- This ensures that the rest of our application can work with a consistent data model.
- The BinanceHelper is introduced here
6. Integrating External Cryptocurrency APIs
Using IHttpClientFactory
Into Program.cs register named HttpClient
instances for each provider. In our case we register for CoinGecko and Binance
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/");
});
CoinGecko Data Provider
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 Data Provider with Symbol Mapping and Optimization
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)
{
var binanceSymbols = cryptoIds.Select(id =>Adapters.BinanceHelper.Instance.GetBinanceSymbol(id)).ToList();
var response = await _httpClient.GetStringAsync("api/v3/ticker/price");
using var jsonDocument = JsonDocument.Parse(response);
var root = jsonDocument.RootElement;
var filteredElements = root.EnumerateArray()
.Where(item =>
{
var symbol = item.GetProperty("symbol").GetString().ToLower();
return binanceSymbols.Contains(symbol, StringComparer.OrdinalIgnoreCase);
})
.ToList();
var filteredResponse = JsonSerializer.Serialize(filteredElements);
var prices = _adapter.AdaptPrices(filteredResponse);
return prices;
}
public async Task<List<CryptoMarketData>> GetCryptoMarketDataAsync(string[] cryptoIds)
{
var tasks = new List<Task<CryptoMarketData>>();
foreach (var cryptoId in cryptoIds)
{
tasks.Add(GetMarketDataForSymbolAsync(cryptoId));
}
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);
var marketData = _adapter.AdaptMarketData(response);
return marketData.FirstOrDefault();
}
}
Here BinanceHelper
a lazy singleton to handle the symbol mapping. It's also used by BinanceAdapter.cs
public class BinanceHelper
{
private static readonly Lazy<BinanceHelper> _instance = new Lazy<BinanceHelper>(() => new BinanceHelper());
private BinanceHelper() { }
public static BinanceHelper Instance => _instance.Value;
public string GetCryptoNameFromSymbol(string symbol)
{
return symbol switch
{
"BTCUSDT" => "Bitcoin",
"ETHUSDT" => "Ethereum",
"XRPUSDT" => "Ripple",
"LTCUSDT" => "Litecoin",
_ => "Unknown"
};
}
public string GetBinanceSymbol(string cryptoId)
{
return cryptoId.ToLower() switch
{
"bitcoin" => "BTCUSDT",
"ethereum" => "ETHUSDT",
"ripple" => "XRPUSDT",
"litecoin" => "LTCUSDT",
_ => throw new ArgumentException($"Unsupported cryptoId: {cryptoId}")
};
}
}
Explanation:
- Symbol Mapping: Binance uses trading pair symbols (e.g., "BTCUSDT") instead of standard crypto IDs.
- Filtering Before Adaptation: We filter the raw data before passing it to the adapter to optimize performance.
- Concurrency in Market Data Retrieval: Since Binance's API requires individual requests per symbol for market data, we fetch data concurrently to improve performance.
Register the providers in the Middle Layer
To integrate both CoinGecko and Binance providers into the application, you need to register their respective HttpClient
, adapters, and data providers in Program.cs
. Here's how you can do that:
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);
});
7. Building the GraphQL API
Setting Up Hot Chocolate
Install the Hot Chocolate package:
Install-Package HotChocolate.AspNetCore
Defining GraphQL Types
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>();
}
}
Implementing the GraphQL Queries
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);
}
}
Configuring the GraphQL Server
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddType<CryptoMarketDataType>();
8. Error Handling and Failover Mechanisms
Implementing Error Handling
Add exception handling within your data providers to catch and handle API errors gracefully.
public async Task<Dictionary<string, decimal>> GetCryptoPricesAsync(string[] cryptoIds)
{
try
{
}
catch (HttpRequestException ex)
{
throw new Exception("Failed to fetch data from the provider.", ex);
}
}
Implementing Failover
Modify the factory to provide a default provider if the requested one fails.
public ICryptoDataProvider GetProvider(string providerName)
{
if (_providers.TryGetValue(providerName, out var provider))
return provider;
throw new ArgumentException($"Provider '{providerName}' not found.");
}
9. Testing with Banana Cake Pop
Setting Up Banana Cake Pop
From Hot Chocolate version 12 onwards, Banana Cake Pop is included as built-in middleware for developers to test and interact with their GraphQL APIs. By default, when you map your GraphQL endpoints using MapGraphQL()
, Banana Cake Pop is automatically served at the /graphql
endpoint. However, in our configuration, we map Banana Cake Pop to /graphql-ui
to separate the development IDE from the production API endpoint.
This approach provides a dedicated interface for testing and exploring our GraphQL schema without interfering with the API's primary /graphql
endpoint, enhancing both security and clarity.
- Install the Banana Cake Pop package:
Install-Package HotChocolate.AspNetCore.BananaCakePop
- Configuring Banana Cake Pop in Program.cs
var app = builder.Build();
app.MapGraphQL("/graphql");
if (app.Environment.IsDevelopment())
{
app.MapBananaCakePop("/graphql-ui");
}
app.Run();
Explanation:
app.MapGraphQL("/graphql")
: Maps the GraphQL API endpoint to /graphql
, which is where client applications will send their queries and mutations. app.MapBananaCakePop("/graphql-ui")
: Maps the Banana Cake Pop GraphQL IDE to /graphql-ui
. This provides developers with a user-friendly interface to test and debug GraphQL queries. - Conditional Middleware Registration: We wrap the Banana Cake Pop mapping inside an
if
statement that checks if the application is running in the development environment. This ensures that the IDE is only available during development and not in production, enhancing security.
Why Map Banana Cake Pop to /graphql-ui
?
By default, Banana Cake Pop would be available at the same endpoint as the GraphQL API (/graphql
). Separating the IDE from the API endpoint has several benefits:
- Security: Exposing development tools in a production environment can be a security risk. By mapping Banana Cake Pop to a different endpoint and limiting it to the development environment, we reduce this risk.
- Clarity: It distinguishes between the API endpoint used by client applications and the development tools used by developers.
- Flexibility: Allows for custom configurations and easier access management, such as applying different authentication or authorization policies to the IDE.
Accessing Banana Cake Pop
After running the application, you can access the Banana Cake Pop IDE by navigating to:
<code>https:
Replace {port}
with the actual port number your application is running on.
Modifying the Launch Profile to Open Banana Cake Pop Automatically
In addition to mapping Banana Cake Pop to a custom endpoint, you can configure your development environment to launch the Banana Cake Pop IDE automatically when you start your application. This is especially convenient for development and testing purposes.
Changing the Launch Profile in Visual Studio
To set up your application to open the /graphql-ui
endpoint automatically upon launching, you can modify the launch settings in Visual Studio:
-
Locate launchSettings.json
:
- In your project, navigate to the
Properties
folder. - Open the
launchSettings.json
file.
-
Modify the Launch URL:
- Find the launch profile you are using, typically under
"profiles"
. - Add or modify the
"launchUrl"
property to include graphql-ui
.
Example:
{
"profiles": {
"CryptoMicroservice": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "graphql-ui",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
- Explanation:
"launchUrl": "graphql-ui"
: Sets the URL path that the browser will navigate to when the application starts. "launchBrowser": true
: Ensures that the browser is launched automatically.
-
Save the Changes:
- After modifying
launchSettings.json
, save the file.
-
Restart the Application:
- Run your application (e.g., press F5 in Visual Studio).
- The browser should now open automatically at
https://localhost:{port}/graphql-ui
.
Benefits of Modifying the Launch Profile
- Convenience: Automatically opens the Banana Cake Pop IDE upon starting the application, saving time during development.
- Efficiency: Streamlines the testing and debugging process by immediately providing access to the GraphQL IDE.
- Focus: Allows you to concentrate on developing and testing your GraphQL queries without manually navigating to the IDE.
Important Notes
- Development Environment Only: Ensure that this configuration is only active in the development environment to avoid unintended behavior in production.
- Multiple Profiles: If you have multiple launch profiles (e.g., for different environments or configurations), make sure to update the appropriate one.
Alternative: Modifying the Debug Properties via Visual Studio UI
If you prefer using the Visual Studio interface:
-
Right-Click the Project:
- In the Solution Explorer, right-click on your project (e.g.,
CryptoMicroservice
). - Select Properties.
-
Navigate to the Debug Tab:
- In the project properties window, click on the Debug tab.
-
Set the Launch URL:
- In the Launch browser section, set the Launch URL to
graphql-ui
.
-
Save and Run:
- Save your changes.
- Run the application, and the browser should open at the Banana Cake Pop IDE.
By modifying the launch profile to include graphql-ui
, you enhance your development workflow, making it quicker and easier to test and debug your GraphQL API using Banana Cake Pop.
Example Usage in Development
With this setup, developers can:
- Explore the Schema: View all the available queries, mutations, and types defined in the GraphQL API.
- Test Queries and Mutations: Write and execute GraphQL operations to test the API's functionality.
- Inspect Responses: Examine the data returned by the API and any error messages or stack traces (if exception details are enabled).
Important Notes
- Production Environment: In production, Banana Cake Pop will not be available, as the
IsDevelopment()
check will fail. This ensures that end-users and potential attackers cannot access the development tools. - Customization: You can customize the path or additional settings of Banana Cake Pop by providing options to the
MapBananaCakePop()
method.
Testing the API
Sample Query: Fetch Crypto Prices
query {
cryptoPrices(cryptoIds: ["bitcoin", "ethereum"], provider: "CoinGecko") {
bitcoin
ethereum
}
}
Sample Query: Fetch Market Data
query {
cryptoMarketData(cryptoIds: ["bitcoin"], provider: "Binance") {
symbol
name
currentPrice
volume
}
}
10. Extending the Microservice with New Providers
Adding a new provider like Kraken involves:
-
Implementing the Adapter:
public class KrakenAdapter : ICryptoDataProviderAdapter
{
public Dictionary<string, decimal> AdaptPrices(string rawData)
{
}
public List<CryptoMarketData> AdaptMarketData(string rawData)
{
}
}
-
Implementing the Data Provider:
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)
{
}
public async Task<List<CryptoMarketData>> GetCryptoMarketDataAsync(string[] cryptoIds)
{
}
}
-
Registering the Provider:
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);
});
No changes are required in the factory or the GraphQL query classes.
11. Conclusion
By leveraging the Strategy and Adapter design patterns, along with a provider Factory, we've built a flexible and extensible .NET 8 microservice that integrates multiple cryptocurrency data providers using GraphQL. This architecture adheres to the Open/Closed Principle of SOLID design, allowing for new providers to be added without modifying existing code. By optimizing data processing and handling provider-specific nuances, we've ensured high performance and reliability. The use of System.Text.Json
enhances performance and reduces dependencies.
In Summary:
- Strategy Pattern: Enables dynamic selection of data providers at runtime, making the system flexible and easily extensible.
- Adapter Pattern: Standardizes data formats from different providers, allowing the application to process data uniformly.
- Factory Pattern: Manages the creation and retrieval of data provider instances, decoupling client code from concrete implementations.
- GraphQL API: Provides a powerful and flexible interface for clients to request data from specific providers.
12. References
Appendix
Models
CryptoMarketData Data Model
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; }
}
Full Program.cs
using CryptoMicroservice.Adapters;
using CryptoMicroservice.DataProviders;
using CryptoMicroservice.Factories;
using CryptoMicroservice.GraphQL;
using CryptoMicroservice.Interfaces;
var builder = WebApplication.CreateBuilder(args);
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/");
});
builder.Services.AddSingleton<ICryptoDataProviderAdapter, CoinGeckoAdapter>();
builder.Services.AddSingleton<ICryptoDataProviderAdapter, BinanceAdapter>();
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);
});
builder.Services.AddSingleton<ICryptoDataProviderFactory, CryptoDataProviderFactory>();
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddType<CryptoMarketDataType>();
var app = builder.Build();
app.MapGraphQL("/graphql");
if (app.Environment.IsDevelopment())
{
app.MapBananaCakePop("/graphql-ui");
}
app.Run();
Happy coding! If you have any questions or need further assistance, feel free to reach out.