Implementing an API Gateway in ASP.NET Core

Building a Simple API Gateway in C# with ASP.NET Core

API Gateways are an essential part of modern microservices-based architectures. They act as intermediaries between clients and multiple backend services, providing a unified entry point for accessing various APIs. In this blog post, we’ll explore how to build a simple API Gateway in C# using ASP.NET Core.

API Gateway
Order Service
Product Service
API Gateway Instance
Order Service Instance
Product Service Instance
Client

Prerequisites

Before we start, make sure you have the following set up on your development machine:

  1. .NET Core SDK installed.
  2. Basic understanding of microservices architecture and RESTful APIs.

Project Setup

  1. Create a new ASP.NET Core Web API project:

    Open a terminal or command prompt, navigate to your desired folder, and run the following command:

    bashCopy code

    dotnet new webapi -n ApiGateway cd ApiGateway

  2. Add necessary NuGet packages:

    For our API Gateway, we’ll need to use System.Net.Http to make requests to backend services and Microsoft.Extensions.Options to handle configurations.

    Run the following commands to add the required packages:

    dotnet add package System.Net.Http
    dotnet add package Microsoft.Extensions.Options
    
  3. Create a class to represent API Gateway settings:

    In the root of the project, create a new folder called “Models,” and within that folder, create a class named ApiGatewaySettings.cs. This class will hold the necessary settings for our gateway.

    using System;
    
    namespace ApiGateway.Models
    {
        public class ApiGatewaySettings
        {
            public string ValidApiKeys { get; set; }
            public string ProductServiceBaseUrl { get; set; }
            public string OrderServiceBaseUrl { get; set; }
        }
    }
    
  4. Configure the appsettings.json file:

    Open the appsettings.json file and add the following configuration for the API Gateway:

    {
      "ApiGatewaySettings": {
        "ValidApiKeys": "YOUR_VALID_API_KEYS_HERE",
        "ProductServiceBaseUrl": "http://localhost:5001", // Replace with actual ProductService base URL
        "OrderServiceBaseUrl": "http://localhost:5002"    
    }
    

Implementing the API Gateway


Now, let’s create the API Gateway controller that will handle requests and proxy them to the appropriate backend services.

  1. Create a new controller named GatewayController.cs:

    Replace the content of Controllers/GatewayController.cs with the following code:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using ApiGateway.Models;

namespace ApiGateway.Controllers
{
 [ApiController]
 [Route("api")]
 public class GatewayController : ControllerBase
 {
     private readonly IHttpClientFactory _httpClientFactory;
     private readonly ApiGatewaySettings _gatewaySettings;

     // Dictionary to store the client API keys
     private readonly HashSet<string> _validApiKeys;

     public GatewayController(IHttpClientFactory httpClientFactory, IOptions<ApiGatewaySettings> gatewaySettings)
     {
         _httpClientFactory = httpClientFactory;
         _gatewaySettings = gatewaySettings.Value;

         // Parse the comma-separated valid API keys from the settings and store them in a HashSet
         _validApiKeys = new HashSet<string>(_gatewaySettings.ValidApiKeys.Split(',').Select(apiKey => apiKey.Trim()));
     }

     private bool IsApiKeyValid(string apiKey)
     {
         // Check if the API key exists in the valid API keys HashSet
         return _validApiKeys.Contains(apiKey);
     }

     [HttpGet("products")]
     public async Task<IActionResult> GetProducts()
     {
         // Check for X-API-Key header
         if (!Request.Headers.TryGetValue("X-API-Key", out var apiKey))
         {
             return BadRequest("X-API-Key header is missing.");
         }

         // Validate the API key
         if (!IsApiKeyValid(apiKey))
         {
             return StatusCode(401, "Invalid API key.");
         }

         var productServiceClient = _httpClientFactory.CreateClient("ProductServiceClient");
         var response = await productServiceClient.GetAsync($"{_gatewaySettings.ProductServiceBaseUrl}/api/products");

         if (response.IsSuccessStatusCode)
         {
             var content = await response.Content.ReadAsStringAsync();
             return Content(content, "application/json");
         }
         else
         {
             return StatusCode((int)response.StatusCode, response.ReasonPhrase);
         }
     }

     [HttpGet("orders")]
     public async Task<IActionResult> GetOrders()
     {
         // Check for X-API-Key header
         if (!Request.Headers.TryGetValue("X-API-Key", out var apiKey))
         {
             return BadRequest("X-API-Key header is missing.");
         }

         // Validate the API key
         if (!IsApiKeyValid(apiKey))
         {
             return StatusCode(401, "Invalid API key.");
         }

         var orderServiceClient = _httpClientFactory.CreateClient("OrderServiceClient");
         var response = await orderServiceClient.GetAsync($"{_gatewaySettings.OrderServiceBaseUrl}/api/orders");

         if (response.IsSuccessStatusCode)
         {
             var content = await response.Content.ReadAsStringAsync();
             return Content(content, "application/json");
         }
         else
         {
             return StatusCode((int)response.StatusCode, response.ReasonPhrase);
         }
     }
 }
}

In this code, we have created two endpoints: GetProducts and GetOrders. Each of these endpoints checks for the presence and validity of an API key in the request header before forwarding the request to the corresponding backend service. The response from the backend service is then returned to the client.

  1. Create OrderService and ProductService:
    Replace the content of Program.cs with the following code:

OrderService

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

public class OrderService
{
    public string GetOrders()
    {
        return "List of orders from OrderService";
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.Configure(app =>
                {
                    app.UseRouting();
                    app.UseEndpoints(endpoints =>
                    {
                        endpoints.MapGet("/api/orders", async context =>
                        {
                            var orderService = new OrderService();
                            await context.Response.WriteAsync(orderService.GetOrders());
                        });
                    });
                });
            });
}

ProductService

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

public class ProductService
{
    public string GetProducts()
    {
        return "List of products from ProductService";
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.Configure(app =>
                {
                    app.UseRouting();
                    app.UseEndpoints(endpoints =>
                    {
                        endpoints.MapGet("/api/products", async context =>
                        {
                            var productService = new ProductService();
                            await context.Response.WriteAsync(productService.GetProducts());
                        });
                    });
                });
            });
}



The ProductService and OrderService classes contain the logic to handle requests for products and orders, respectively. For simplicity, we return static strings in the GetProducts and GetOrders methods, but in real-world scenarios, these methods would interact with databases or external APIs.

  1. Register the HTTP clients and settings in Startup.cs:

Open the Startup.cs file and add the following configuration inside the ConfigureServices method:

using ApiGateway.Models;
public class Program
{
  public static void Main(string[] args)
  {
      CreateHostBuilder(args).Build().Run();
  }

  public static IHostBuilder CreateHostBuilder(string[] args) =>
      Host.CreateDefaultBuilder(args)
          .ConfigureWebHostDefaults(webBuilder =>
          {
              webBuilder.ConfigureServices(services =>
              {
                  services.AddHttpClient("ProductServiceClient", c => c.BaseAddress = new Uri("http://localhost:5001"));
                  services.AddHttpClient("OrderServiceClient", c => c.BaseAddress = new Uri("http://localhost:5002"));
                  services.Configure<ApiGatewaySettings>(options =>
                  {
                      var configuration = new ConfigurationBuilder()
                                  .SetBasePath(Directory.GetCurrentDirectory())
                                  .AddJsonFile("appsettings.json")
                                  .Build();
                      options.ProductServiceBaseUrl = configuration["ApiGatewaySettings:ProductServiceBaseUrl"];
                      options.OrderServiceBaseUrl = configuration["ApiGatewaySettings:OrderServiceBaseUrl"];
                      options.ValidApiKeys = configuration["ApiGatewaySettings:ValidApiKeys"];
                     
                  });
                  services.AddControllers();
              });

              webBuilder.Configure(app =>
              {
                  app.UseRouting();
                  app.UseEndpoints(endpoints =>
                  {
                      endpoints.MapControllers();
                  });
              });
          });
}
  1. Test your API Gateway:

    Now, you can run your API Gateway using the following command:

    dotnet run
    

    Once the gateway is running, you can send requests to it. For example, you can use tools like cURL or Postman to test the GetProducts and GetOrders endpoints.


GET http://localhost:5000/api/products
X-API-Key: YOUR_API_KEY

  GET http://localhost:5000/api/orders
  X-API-Key: YOUR_API_KEY

Conclusion

In this blog post, we have built a simple API Gateway in C# using ASP.NET Core. The gateway acts as an intermediary between clients and backend services, providing a unified entry point to access different APIs. By leveraging the power of ASP.NET Core and HttpClient, we have created a basic gateway capable of handling requests and responses.

Keep in mind that this is a minimal implementation for educational purposes. In a real-world scenario, you may need to add additional features such as caching, rate limiting, load balancing, and more.

Happy coding!

Next Post Previous Post
No Comment
Add Comment
comment url