Click here to Skip to main content
16,004,507 members
Articles / Programming Languages / C#
Article

Implementing Strategy and Adapter Patterns in a .NET Microservice for Multiple Cryptocurrency Data Providers with GraphQL

Rate me:
Please Sign up or sign in to vote.
4.64/5 (4 votes)
14 Sep 2024MIT14 min read 4.7K   9  
Implementing Strategy and Adapter Patterns in a .NET 8 microservice to integrate multiple cryptocurrency data providers with GraphQL for a dynamic and scalable architecture.
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:

  1. 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.
  2. 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).
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. Create a New Project: Open Visual Studio and create a new ASP.NET Core Web API project named CryptoMicroservice targeting .NET 8.0.

  2. Disable Controllers: Uncheck the Use controllers option, as we'll be using GraphQL.

  3. Add NuGet Packages

    Shell
    Install-Package HotChocolate.AspNetCore
    Install-Package System.Text.Json
  4. 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.

C#
// Interfaces/ICryptoDataProvider.cs
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.

C#
// 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.");
    }
}

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:

C#
// program.cs
// Register the Factory
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.

C#
// Interfaces/ICryptoDataProviderAdapter.cs
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

C#
// 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 Adapter

// 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;
     }
    
 }

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

C#
// 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
});

Implementing Data Providers

CoinGecko Data Provider

C#
// 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 Data Provider with Symbol Mapping and Optimization

C#
// 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();
     }
   
 }

Here BinanceHelper a lazy singleton to handle the symbol mapping. It's also used by BinanceAdapter.cs

C#
// 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}")
         };
     }
 }

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:

C#
// 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. Building the GraphQL API

Setting Up Hot Chocolate

Install the Hot Chocolate package:

Shell
Install-Package HotChocolate.AspNetCore

Defining GraphQL Types

C#
// 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>();
    }
}

Implementing the GraphQL Queries

// 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);
    }
}

Configuring the GraphQL Server

C#
// Program.cs
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.

C#
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);
    }
}

Implementing Failover

Modify the factory to provide a default provider if the requested one fails.

C#
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. 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:
Shell
Install-Package HotChocolate.AspNetCore.BananaCakePop
  • Configuring Banana Cake Pop in Program.cs
C#
// 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();

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://localhost:{port}/graphql-ui</code>

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:

  1. Locate launchSettings.json:

    • In your project, navigate to the Properties folder.
    • Open the launchSettings.json file.
  2. 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.
  3. Save the Changes:

    • After modifying launchSettings.json, save the file.
  4. 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:

  1. Right-Click the Project:

    • In the Solution Explorer, right-click on your project (e.g., CryptoMicroservice).
    • Select Properties.
  2. Navigate to the Debug Tab:

    • In the project properties window, click on the Debug tab.
  3. Set the Launch URL:

    • In the Launch browser section, set the Launch URL to graphql-ui.
  4. 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

graphql
 query {
  cryptoPrices(cryptoIds: ["bitcoin", "ethereum"], provider: "CoinGecko") {
    bitcoin
    ethereum
  }
}

Sample Query: Fetch Market Data

graphql
 query {
  cryptoMarketData(cryptoIds: ["bitcoin"], provider: "Binance") {
    symbol
    name
    currentPrice
    volume
  }
}

 

10. Extending the Microservice with New Providers

Adding a new provider like Kraken involves:

  1. Implementing the Adapter:

    C#
     // 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
        }
    }
  2. Implementing the Data Provider:

    C#
     // 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
        }
    }
  3. Registering the Provider:

    C#
    // 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);
    });
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

C#
// 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; }
}

Full Program.cs

C#
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();

 

Happy coding! If you have any questions or need further assistance, feel free to reach out.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Architect
Switzerland Switzerland
I really like coding and continually strive to improve my knowledge of design patterns and emerging architectures. My journey began 41 years ago, when at the age of 13, I bought my first Sinclair ZX Spectrum and started teaching myself to code. Today, I am a Senior Solution Architect with expertise in Agile methodologies, specializing in the design and development of Enterprise Applications across both back-end and front-end. I have extensive experience working in the Fintech and Medtech domains.

Over the years, I have developed a keen interest in Enterprise Design Patterns, Domain Driven Design, Test Driven Design, and Scrum Methodology. I’m always on the lookout for new patterns that can enhance software development practices. I am also passionate about AI, leveraging machine learning and artificial intelligence technologies to optimize solutions and automate processes within enterprise systems. My AI experience ranges from integrating intelligent algorithms into applications to enhancing user experiences with predictive models.

I believe knowledge should be shared, so I actively contribute to the coding community through platforms like Code Project, sharing insights that may benefit other software engineers and developers in this incredible industry. I don’t claim to have all the answers, but the practices I’ve adopted have played a significant role in my own career. If they resonate with you, feel free to try them. And if you have any comments—positive or constructive—I’d love to hear from you.
This is a Collaborative Group

10 members

Comments and Discussions

 
-- There are no messages in this forum --