API Gateway using YARP in ASP.NET Core

YARP API Gateway in ASP.NET Core Web API

In our Microservices architecture (User, Product, Order, Payment, Notification), the API Gateway serves as the Single-Entry Point for all client requests. While Ocelot is a mature choice for .NET developers, Microsoft introduced YARP (Yet Another Reverse Proxy) as a modern, highly customizable, high-performance alternative that integrates natively with ASP.NET Core’s middleware pipeline.

YARP acts as a Reverse Proxy and Load Balancer, routing, transforming, and securing requests between clients and backend microservices.

What is YARP (Yet Another Reverse Proxy)?

YARP (Yet Another Reverse Proxy) is a high-performance, highly extensible reverse proxy library built by Microsoft on top of ASP.NET Core. It acts as an intelligent intermediary between clients and backend microservices, handling request routing, Load Balancing, Health Checks, authentication, and response transformation.

In simple terms, YARP serves as the central Gateway Layer in a microservices architecture. It accepts incoming HTTP requests from clients, examines the route configuration, applies any defined transformations (such as path modification, header injection, or authentication validation), and forwards the request to the appropriate downstream service. When the response returns, YARP can apply additional rules, such as compression or caching, before sending it back to the client.

Why Choose YARP Over Ocelot?

Although both YARP and Ocelot are popular API Gateway solutions for .NET-based microservices, YARP is a more modern, flexible, and efficient framework. Ocelot is a great option for small to medium-sized projects, but YARP is designed explicitly for enterprise-grade applications where performance, extensibility, and tight ASP.NET integration are crucial.

Here are the main reasons why YARP is often preferred over Ocelot:

  • Official Microsoft Support: YARP is actively maintained by Microsoft, ensuring long-term stability, compatibility with newer .NET versions, and frequent performance improvements.
  • High Performance: It can handle thousands of concurrent requests efficiently, with minimal latency and resource consumption.
  • Flexible Configuration Options: You can configure YARP through a reverseproxy.json file. This makes it easier to modify routes or clusters at runtime without restarting the gateway.
  • Advanced Extensibility: YARP supports custom requests, response transforms, middleware injection, and runtime policies for load balancing, security, and routing.
  • Better Integration for Enterprise Use: It integrates seamlessly with .NET’s dependency injection, logging, and authentication frameworks, making it easier to maintain consistency across all microservices.

In short, YARP is preferred for building modern, scalable, production-grade microservice API gateways that require maximum flexibility and performance.

Integrating YARP in ASP.NET Core API Gateway

Integrating YARP into an ASP.NET Core API Gateway is straightforward and similar to Ocelot, but with more flexibility and less configuration overhead. The following is a step-by-step explanation of the integration process.

Step 1: Install the YARP NuGet Package

In the APIGateway project, open Package Manager Console and install the official YARP package. This adds the core YARP middleware for proxying requests and configuring routes/clusters.

  • Install-Package Yarp.ReverseProxy
Step 2: Add reverseproxy.json File

Inside your API Gateway project, create a reverseproxy.json configuration file similar to your Ocelot setup, defining routes and clusters for each microservice. Once you have created the reverseproxy.json file, add the following code.

{
  "ReverseProxy": {
    "Routes": {
      "userRoute": {
        "ClusterId": "userCluster",
        "Match": {
          "Path": "/users/{**catch-all}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/users" },
          { "PathPrefix": "/api" }
        ]
      },
      "productRoute": {
        "ClusterId": "productCluster",
        "Match": {
          "Path": "/products/{**catch-all}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/products" },
          { "PathPrefix": "/api" }
        ]
      },
      "orderRoute": {
        "ClusterId": "orderCluster",
        "Match": {
          "Path": "/orders/{**catch-all}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/orders" },
          { "PathPrefix": "/api" }
        ]
      },
      "paymentRoute": {
        "ClusterId": "paymentCluster",
        "Match": {
          "Path": "/payments/{**catch-all}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/payments" },
          { "PathPrefix": "/api" }
        ]
      },
      "notificationRoute": {
        "ClusterId": "notificationCluster",
        "Match": {
          "Path": "/notifications/{**catch-all}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/notifications" },
          { "PathPrefix": "/api" }
        ]
      }
    },

    "Clusters": {
      "userCluster": {
        "Destinations": {
          "userApi": { "Address": "https://localhost:7269" }
        }
      },
      "productCluster": {
        "Destinations": {
          "productApi": { "Address": "https://localhost:7234" }
        }
      },
      "orderCluster": {
        "Destinations": {
          "orderApi": { "Address": "https://localhost:7082" }
        }
      },
      "paymentCluster": {
        "Destinations": {
          "paymentApi": { "Address": "https://localhost:7123" }
        }
      },
      "notificationCluster": {
        "Destinations": {
          "notificationApi": { "Address": "https://localhost:7254" }
        }
      }
    }
  }
}
Code Explanations:

Once you save this file, YARP will use it to know how to forward requests to your different services. Here’s what each part means:

  • Path defines the pattern of the Incoming Request URL (the one the client calls). For example, if the client requests /users/getall, YARP will match it to the route defined under “Path”: “/users/{**catch-all}”.
  • ClusterId links the route to a specific cluster (the actual backend microservice). Each route must specify which cluster it belongs to.
  • The Address defines the Actual Downstream URL of the microservice that will handle the request. This is where YARP forwards the request after applying transformations.
  • **{catch-all} is similar to Ocelot’s {everything} placeholder. It ensures that any path following /users/, /orders/, etc., is automatically captured and forwarded to the correct microservice endpoint.
  • Transforms modify the request path before forwarding. For example, PathRemovePrefix removes /users from the incoming URL, and PathPrefix adds /api, meaning the gateway’s /users/getall becomes /api/getall when calling the downstream service.

This configuration gives your gateway full control over how requests are routed and transformed across all microservices.

Step 3: Register YARP in the Program.cs Class

Now, we need to replace the Ocelot middleware with the YARP configuration. So, please modify the Program class as follows.

using APIGateway.Extensions;
using APIGateway.Middlewares;
using APIGateway.Models;
using APIGateway.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Serialization;
using Serilog;
using System.Text;

namespace APIGateway
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // MVC Controllers + Newtonsoft JSON Configuration
            builder.Services
                .AddControllers()
                .AddNewtonsoftJson(options =>
                {
                    options.SerializerSettings.ContractResolver = new DefaultContractResolver
                    {
                        NamingStrategy = new DefaultNamingStrategy()
                    };
                });

            // Load reverseproxy.json (YARP)
            builder.Configuration.AddJsonFile(
                "reverseproxy.json",
                optional: false,
                reloadOnChange: true
            );

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

            // Reads the RateLimiting section from appsettings.json
            // Binds it to your RateLimitSettings model
            // Registers the RateLimitPolicyService as a singleton
            builder.Services.AddCustomRateLimiting(builder.Configuration);

            // Bind CompressionSettings section to our model using opions pattern
            builder.Services.Configure<CompressionSettings>(
                builder.Configuration.GetSection("CompressionSettings"));

            // Ocelot Configuration (API Gateway Routing Layer)
            // Comment the following
            //builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
            //builder.Services.AddOcelot(builder.Configuration);

            // Structured Logging Setup (Serilog)
            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(builder.Configuration)
                .Enrich.FromLogContext()
                .CreateLogger();

            builder.Host.UseSerilog(); // Replace default .NET logger with Serilog.

            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // JWT Authentication (Bearer Token Validation)
            builder.Services
                .AddAuthentication(options =>
                {
                    // Define the default authentication scheme as Bearer
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(options =>
                {
                    // Token validation configuration
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidIssuer = builder.Configuration["JwtSettings:Issuer"],

                        // We’re not validating audience because microservices share same gateway.
                        ValidateAudience = false,

                        // Enforce token expiry check
                        ValidateLifetime = true,

                        // Ensure token signature integrity using secret key
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(
                            Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:SecretKey"]!)
                        ),

                        // No extra grace period for expired tokens
                        ClockSkew = TimeSpan.Zero
                    };
                });

            builder.Services.AddAuthorization(); // Enables [Authorize] attributes.

            // Downstream Microservice Clients (typed HttpClientFactory)
            var urls = builder.Configuration.GetSection("ServiceUrls");

            builder.Services.AddHttpClient("OrderService", c =>
            {
                c.BaseAddress = new Uri(urls["OrderService"]!);
            });

            builder.Services.AddHttpClient("UserService", c =>
            {
                c.BaseAddress = new Uri(urls["UserService"]!);
            });

            builder.Services.AddHttpClient("ProductService", c =>
            {
                c.BaseAddress = new Uri(urls["ProductService"]!);
            });

            builder.Services.AddHttpClient("PaymentService", c =>
            {
                c.BaseAddress = new Uri(urls["PaymentService"]!);
            });

            // Register IHttpContextAccessor
            builder.Services.AddHttpContextAccessor();

            // Custom Aggregation Service Registration
            builder.Services.AddScoped<IOrderSummaryAggregator, OrderSummaryAggregator>();

            // Register Redis as the distributed caching provider for the API Gateway.
            //    This allows our middleware (and any service) to store and retrieve cache data 
            //    in a centralized Redis instance instead of in-memory cache.
            builder.Services.AddStackExchangeRedisCache(options =>
            {
                // The connection string defines how our app connects to the Redis server.
                //    Example format: "localhost:6379" (for local Redis)
                //    or "redis:6379,password=yourpassword,ssl=False,abortConnect=False" (for containerized/remote setup)
                //    The value is read from appsettings.json → "RedisCacheSettings:ConnectionString".
                options.Configuration = builder.Configuration["RedisCacheSettings:ConnectionString"];

                // The instance name is an optional logical prefix used to differentiate keys
                //    when multiple applications share the same Redis server.
                //    Example: If InstanceName = "ApiGateway_", all cache keys will start with that prefix.
                //    This helps prevent key collisions between different microservices or environments.
                options.InstanceName = builder.Configuration["RedisCacheSettings:InstanceName"];
            });

            var app = builder.Build();

            // Swagger (API Explorer for development/debugging)
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            // 1. Correlation ID (MUST RUN FIRST)
            app.UseCorrelationId();

            // 2. Logging (request/response)
            app.UseRequestResponseLogging();

            // 3. Authentication (Validate JWT Signature)
            app.UseAuthentication();

            // 4. Validate token BEFORE proxying
            app.UseGatewayBearerValidation();

            // 5. Rate Limiting (after authentication → per user)
            app.UseCustomRateLimiting();

            // 6. Compression (must be LAST before proxy)
            app.UseMiddleware<ConditionalResponseCompressionMiddleware>();

            // 7. Redis Cache (must run before compression)
            app.UseRedisResponseCaching();

            // BRANCH 1: Custom Aggregated Endpoints (/gateway/*)
            app.MapWhen(
                ctx => ctx.Request.Path.StartsWithSegments("/gateway", StringComparison.OrdinalIgnoreCase),
                gatewayApp =>
                {
                    // Enable endpoint routing for this sub-pipeline
                    gatewayApp.UseRouting();

                    // Apply authentication & authorization
                    gatewayApp.UseAuthentication();
                    gatewayApp.UseAuthorization();

                    // Apply rate limiting also inside this sub-pipeline if needed
                    gatewayApp.UseCustomRateLimiting();

                    // Register controller actions under this branch
                    gatewayApp.UseEndpoints(endpoints =>
                    {
                        endpoints.MapControllers();
                    });
                });

            // BRANCH 2: YARP Reverse Proxy
            // YARP must be last
            app.MapReverseProxy();

            // Ocelot middleware handles routing, transformation, and load-balancing
            // Comment the following
            // await app.UseOcelot();

            // Start the Application
            app.Run();
        }
    }
}

Implementing the API Gateway using YARP (Yet Another Reverse Proxy) provides a modern, high-performance, and extensible solution for managing communication between clients and microservices in an ASP.NET Core environment. With its simple configuration, dynamic routing, and official Microsoft support, YARP enables developers to build scalable, secure, and efficient API gateways that deliver better performance and reliability for enterprise-grade microservice architectures.

Leave a Reply

Your email address will not be published. Required fields are marked *