Skip to main content

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy:

  • Different base URLs
  • Duplicated auth logic
  • No unified rate limiting / caching
  • Hard to evolve routes or aggregate data

👉 Enter the API Gateway — your single front door for all microservices.

An API Gateway handles:

✅ Routing & path rewriting
✅ Load balancing, retries, circuit breakers
✅ Authentication & Authorization (JWT, OAuth2)
✅ Rate limiting & caching
✅ Aggregation (compose results from multiple services)

In this post we’ll implement two strong options:

  • Ocelot → config-driven, mature, DevOps-friendly
  • YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway

⚖️ Ocelot vs YARP — When to Choose

  • Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code.
  • YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashboards, policies).

Both are production-ready ✅


🛠 Example Setup

Assume services running locally:

We’ll build a gateway at http://localhost:5000 exposing:

  • GET /api/products → product-service /products
  • GET /api/orders → order-service /orders
  • GET /api/market → aggregate both (Ocelot demo)

🔹 Option A: Ocelot (Config-First Gateway)

1) Create Gateway Project

dotnet new web -n ApiGateway.Ocelot
cd ApiGateway.Ocelot
dotnet add package Ocelot
dotnet add package Ocelot.Provider.Consul         # optional, service discovery
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

2) Core Routing Config

📄 ocelot.json

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/products",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [{ "Host": "localhost", "Port": 5001 }],
      "UpstreamPathTemplate": "/api/products",
      "UpstreamHttpMethod": [ "GET" ]
    },
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [{ "Host": "localhost", "Port": 5002 }],
      "UpstreamPathTemplate": "/api/orders",
      "UpstreamHttpMethod": [ "GET" ]
    }
  ],
  "GlobalConfiguration": { "BaseUrl": "http://localhost:5000" }
}

3) Program.cs

using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Microsoft.AspNetCore.Authentication.JwtBearer;

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.Authority = "https://your-identity-server";
        o.Audience = "api";
        o.RequireHttpsMetadata = false; // dev only
    });

builder.Services.AddOcelot();

var app = builder.Build();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

await app.UseOcelot(); // must be last
app.Run();

Run:

dotnet run
curl http://localhost:5000/api/products
curl http://localhost:5000/api/orders

4) Add QoS & Rate Limiting

Update route in ocelot.json:

"QoSOptions": {
  "ExceptionsAllowedBeforeBreaking": 3,
  "DurationOfBreak": 10000,
  "TimeoutValue": 2000
},
"RateLimitOptions": {
  "EnableRateLimiting": true,
  "Period": "1s",
  "Limit": 10
}

➡ Timeout 2s, break after 3 failures for 10s
➡ Limit 10 requests/second


5) Aggregation Example

Combine products + orders:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/products",
      "UpstreamPathTemplate": "/internal/products",
      "UpstreamHttpMethod": [ "GET" ],
      "Key": "products"
    },
    {
      "DownstreamPathTemplate": "/orders",
      "UpstreamPathTemplate": "/internal/orders",
      "UpstreamHttpMethod": [ "GET" ],
      "Key": "orders"
    },
    {
      "UpstreamPathTemplate": "/api/market",
      "UpstreamHttpMethod": [ "GET" ],
      "Aggregator": "MarketAggregator",
      "RouteKeys": [ "products", "orders" ]
    }
  ],
  "GlobalConfiguration": { "BaseUrl": "http://localhost:5000" }
}

📄 MarketAggregator.cs

public class MarketAggregator : IDefinedAggregator
{
    public async Task<Response> Aggregate(List<HttpContext> responses)
    {
        var result = new
        {
            products = await JsonSerializer.DeserializeAsync<object>(responses[0].Response.Body),
            orders = await JsonSerializer.DeserializeAsync<object>(responses[1].Response.Body)
        };

        var json = JsonSerializer.Serialize(result);
        var bytes = Encoding.UTF8.GetBytes(json);

        var context = responses.First();
        context.Response.Headers["Content-Type"] = "application/json";
        await context.Response.Body.WriteAsync(bytes);

        return new OkResponse();
    }
}

Register in Program.cs:

builder.Services.AddOcelot().AddSingletonDefinedAggregator<MarketAggregator>();

Now:

curl http://localhost:5000/api/market

6) Service Discovery (Consul)

"ServiceDiscoveryProvider": {
  "Type": "Consul",
  "Host": "localhost",
  "Port": 8500
}

Route example:

{
  "ServiceName": "product-service",
  "DownstreamPathTemplate": "/products",
  "UseServiceDiscovery": true,
  "UpstreamPathTemplate": "/api/products",
  "UpstreamHttpMethod": [ "GET" ]
}

🔹 Option B: YARP (Code-First Gateway)

1) Create Project

dotnet new web -n ApiGateway.Yarp
cd ApiGateway.Yarp
dotnet add package Yarp.ReverseProxy
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

2) Config

📄 appsettings.json

"ReverseProxy": {
  "Routes": {
    "products": {
      "ClusterId": "products-cluster",
      "Match": { "Path": "/api/products/{**catch-all}" },
      "Transforms": [ { "PathRemovePrefix": "/api" } ]
    },
    "orders": {
      "ClusterId": "orders-cluster",
      "Match": { "Path": "/api/orders/{**catch-all}" },
      "Transforms": [ { "PathRemovePrefix": "/api" } ]
    }
  },
  "Clusters": {
    "products-cluster": {
      "Destinations": {
        "d1": { "Address": "http://localhost:5001/" }
      }
    },
    "orders-cluster": {
      "Destinations": {
        "d1": { "Address": "http://localhost:5002/" }
      }
    }
  }
}

3) Program.cs

using Yarp.ReverseProxy;
using Microsoft.AspNetCore.Authentication.JwtBearer;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.Authority = "https://your-identity-server";
        o.Audience = "api";
    });

builder.Services.AddHttpClient("yarp")
    .AddTransientHttpErrorPolicy(p =>
        p.WaitAndRetryAsync(new[] { TimeSpan.FromMilliseconds(200), TimeSpan.FromSeconds(1) }));

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// Propagate correlation ID
app.Use(async (ctx, next) =>
{
    if (!ctx.Request.Headers.ContainsKey("X-Correlation-ID"))
        ctx.Request.Headers["X-Correlation-ID"] = Guid.NewGuid().ToString("n");
    await next();
});

app.MapReverseProxy();
app.Run();

🔐 Auth Patterns at the Gateway

  • Validate tokens once at gateway
  • Forward claims downstream via:
    • Original Authorization header
    • Short-lived downstream token
    • Custom headers (inside trusted networks)

🚀 Extra Features

  • Caching & compression (Ocelot built-in, YARP via middleware)
  • Observability: add OpenTelemetry at the gateway for full latency breakdown
  • Timeouts & retries: fail fast, avoid cascading slowness
  • Security: add HSTS, CORS, CSP headers

🐳 Docker Compose (Quick Local Stack)

📄 docker-compose.yml

version: "3.9"
services:
  product-service:
    image: yourrepo/product-service:latest
    ports: [ "5001:80" ]

  order-service:
    image: yourrepo/order-service:latest
    ports: [ "5002:80" ]

  api-gateway:
    build: ./ApiGateway.Ocelot
    environment:
      - ASPNETCORE_URLS=http://+:80
    ports:
      - "5000:80"
    depends_on:
      - product-service
      - order-service

Test:

curl http://localhost:5000/api/products
curl http://localhost:5000/api/orders

✅ Production Tips

  • Keep gateway thin → only routing, auth, policies
  • Use timeouts & circuit breakers → don’t let one bad service block all
  • Rate limit per API key/tenant → protect downstreams
  • Versioned routes → /api/v1/*, /api/v2/*
  • Zero-trust → validate at edge, re-issue constrained tokens
  • Observability → traces across Gateway → Service → DB

🎯 Recap

  • Ocelot → config-driven, quick setup, built-in QoS
  • YARP → code-first, ultimate extensibility
  • Both → handle auth, routing, load balancing, caching

👉 The API Gateway is your secure, observable front door.

Next up Link → Part 10: Event-Driven Microservices with RabbitMQ/Kafka


Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...