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.
Prerequisites
Before we start, make sure you have the following set up on your development machine:
- .NET Core SDK installed.
- Basic understanding of microservices architecture and RESTful APIs.
Project Setup
-
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
-
Add necessary NuGet packages:
For our API Gateway, we’ll need to use
System.Net.Http
to make requests to backend services andMicrosoft.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
-
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; } } }
-
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.
-
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.
- Create OrderService and ProductService:
Replace the content ofProgram.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.
- 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();
});
});
});
}
-
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
andGetOrders
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!