6 minute read

11 min read 2201 words

As modern application architectures shift towards microservices, the need for a central entry point to manage traffic becomes crucial. This is where an API Gateway comes into play. In this post, we’ll explore YARP (Yet Another Reverse Proxy), a highly customizable reverse proxy built on ASP.NET Core, and how to set it up in .NET 10.


1. What is an API Gateway?

An API Gateway is a server that acts as an entry point for all client requests to your backend services. Instead of clients calling each microservice directly, they send requests to the API Gateway, which then routes the requests to the appropriate service.

Why do we need it?

Think of an API Gateway as the “front door” of your system. Here are the key reasons why it’s essential:

  1. Centralized Entry Point: Simplifies client-side development by providing a single URL for all backend functionality.
  2. Security: It’s the perfect place to handle authentication and authorization before requests reach internal services.
  3. Load Balancing: It can distribute incoming traffic across multiple instances of a service to ensure high availability.
  4. SSL Termination: Offloads the overhead of SSL/TLS encryption/decryption from backend services.
  5. Cross-Cutting Concerns: Handles logging, caching, rate limiting, and request transformation in one place.

Upstream vs. Downstream: Which is Which?

In the context of API Gateways and proxies, you’ll often hear the terms Upstream and Downstream. To keep them straight, imagine the flow of a river:

  • Upstream: This refers to the Client (e.g., a web browser or mobile app) that initiates the request. From the perspective of the gateway, the client is “upstream.”
  • Downstream: This refers to the Backend Services (microservices) that receive the forwarded request. From the perspective of the gateway, it is sending the request “downstream.”

Think of the Gateway as the middleman in the river: the request flows from the client (Upstream) -> through the Gateway -> to the backend service (Downstream).


2. What is YARP?

YARP (Yet Another Reverse Proxy) is an open-source project from Microsoft. Unlike other reverse proxies like Nginx or HAProxy, YARP is built on top of the ASP.NET Core infrastructure.

What is a Reverse Proxy?

A Reverse Proxy is a server that sits in front of one or more web servers and intercepts requests from clients. This is different from a forward proxy, which sits in front of clients.

YARP acts as this intermediary, receiving requests and deciding which backend service should handle them. This allows it to provide:

  • Abstraction: Clients only see the gateway, not the individual microservices.
  • Security: Hide the internal network structure and provide a single point for SSL termination and firewalling.
  • Control: Easily manage traffic flow, versions (canary releases), and maintenance windows.

Why YARP?

  • Performance: It leverages the high-performance Kestrel web server.
  • Customization: Since it’s built in C#, you can easily customize its behavior using standard .NET middleware and code.
  • Integration: It fits naturally into the .NET ecosystem, using familiar configuration patterns (appsettings.json, dependency injection, etc.).

3. Setting Up YARP in .NET 10

Let’s walk through a basic setup to turn an ASP.NET Core application into an API Gateway.

Step 1: Create a new project

First, create an empty ASP.NET Core project using the CLI:

dotnet new web -n MyApiGateway
cd MyApiGateway

Step 2: Add the YARP NuGet Package

Add the Yarp.ReverseProxy package to your project:

dotnet add package Yarp.ReverseProxy

Step 3: Configure Services and Middleware

In .NET 10, the configuration is still handled in Program.cs. We need to register the YARP services and map the proxy middleware.

var builder = WebApplication.CreateBuilder(args);

// Add YARP services and load configuration from appsettings.json
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();

// Map YARP routes
app.MapReverseProxy();

app.Run();

4. Configuring Routes and Clusters

YARP uses two main concepts: Routes and Clusters.

  • Routes: Define which incoming requests should be proxied (based on path, host, methods, etc.).
  • Clusters: Define the backend services (destinations) where the requests should be sent.

Open your appsettings.json and add the following configuration:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ReverseProxy": {
    "Routes": {
      "service1_route": {
        "ClusterId": "cluster1",
        "Match": {
          "Path": "/api/products/{**catch-all}"
        }
      },
      "service2_route": {
        "ClusterId": "cluster2",
        "Match": {
          "Path": "/api/users/{**catch-all}"
        }
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:5001/"
          }
        }
      },
      "cluster2": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:6001/"
          }
        }
      }
    }
  }
}

Breaking down the config:

  1. Routes:
    • service1_route: Matches any request starting with /api/products/ and sends it to cluster1.
  2. Clusters:
    • cluster1: Contains a single destination pointing to our product service running on port 5001.

5. Advanced Features in .NET 10

One of YARP’s greatest strengths is how easily it integrates with standard ASP.NET Core features. In .NET 10, these integrations are more seamless than ever.

5.1 Setting up JWT Authentication

To secure your routes using JWT (JSON Web Tokens), you first need to configure authentication in Program.cs.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AuthenticatedUser", policy => policy.RequireAuthenticatedUser());
});

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

var app = builder.Build();

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

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

In your appsettings.json, you can then apply this policy to specific routes:

"service1_route": {
  "ClusterId": "cluster1",
  "AuthorizationPolicy": "AuthenticatedUser",
  "Match": { "Path": "/api/products/{**catch-all}" }
}

5.2 Rate Limiting

Rate limiting protects your backend services from being overwhelmed by too many requests. ASP.NET Core provides a built-in rate-limiting middleware that YARP supports natively.

Register in Program.cs:

// Required for maintaining rate limiting state
builder.Services.AddMemoryCache();

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", opt =>
    {
        opt.Window = TimeSpan.FromSeconds(10);
        opt.PermitLimit = 5; // Allow 5 requests every 10 seconds
    });
});

// ... inside app ...
app.UseRateLimiter();

Note on State Management: While basic in-memory rate limiting can work without it, adding AddMemoryCache() (or a distributed cache like Redis in production) is crucial for maintaining state correctly, especially when using complex partitioning or running multiple gateway instances.

Apply in appsettings.json:

"service1_route": {
  "ClusterId": "cluster1",
  "RateLimiterPolicy": "fixed",
  "Match": { "Path": "/api/products/{**catch-all}" }
}

5.3 Circuit Breaker and Resilience

In .NET 10, resilience is handled through the Microsoft.Extensions.Resilience library, which is built on Polly v8. You can define a resilience pipeline and apply it to your YARP clusters.

Setup in Program.cs:

using Microsoft.Extensions.Http.Resilience;

builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
    .AddResilienceHandler("my-resilience-handler", pipeline =>
    {
        pipeline.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            FailureRatio = 0.5,
            SamplingDuration = TimeSpan.FromSeconds(10),
            MinimumThroughput = 10,
            BreakDuration = TimeSpan.FromSeconds(30)
        });
    });

Apply to a Cluster in appsettings.json:

"Clusters": {
  "cluster1": {
    "HttpClient": {
      "ResilienceHandler": "my-resilience-handler"
    },
    "Destinations": {
      "destination1": { "Address": "https://localhost:5001/" }
    }
  }
}

5.4 Request Aggregation (API Composition)

Sometimes a client needs data from multiple services (e.g., product details and customer reviews) to render a single page. Instead of the client making multiple round-trips, the API Gateway can aggregate these requests into one.

Since YARP is built on ASP.NET Core, you can easily implement this by defining a custom endpoint that calls multiple backend services and merges the results.

Implementation in Program.cs:

using System.Net.Http.Json;

// Register HttpClient services
builder.Services.AddHttpClient();

// ... inside app ...

app.MapGet("/api/product-summary/{id}", async (string id, IHttpClientFactory clientFactory) =>
{
    var client = clientFactory.CreateClient();

    // Fetch data from multiple services in parallel
    var productTask = client.GetFromJsonAsync<dynamic>($"https://localhost:5001/api/products/{id}");
    var reviewsTask = client.GetFromJsonAsync<dynamic>($"https://localhost:6001/api/reviews/{id}");

    await Task.WhenAll(productTask, reviewsTask);

    return Results.Ok(new
    {
        Product = await productTask,
        Reviews = await reviewsTask
    });
});

6. Further Reading & References

To dive deeper into YARP and building resilient API gateways, check out these official resources:


Summary

Setting up an API Gateway with YARP in .NET 10 is straightforward yet incredibly powerful. It provides:

  • A familiar development experience for .NET developers.
  • High-performance routing using Kestrel.
  • Built-in support for authentication, rate limiting, resilience, and easy request aggregation.
  • The flexibility to customize every aspect of the proxy process using C#.

Whether you’re building a simple microservices architecture or a complex enterprise system, YARP is an excellent choice for your gateway needs!

Leave a comment