ASP.NET Core: Using YARP (Yet Another Reverse Proxy) for ASP.NET Core Apps to create API Gateways

In this article, we will understand and use the YARP (Yet Another Reverse Proxy). This is highly customizable reverse proxy library for .NET. This is especially designed to provide easy but yes secure, scalable, flexible and robust framework for modern ASP.NET Core API Applications. The ASP.NET Core is one of the bests technologies to design, develop highly powerful, secure, and high performant web applications. Being a cross-platform technology, we can build complex server-side solutions using ASP.NET Core. Once of such complex solution is building Microservices application or the server-side application with multiple autonomous APIs. In such solutions we need to host these Microservices or autonomous APIs on separate hosts with separate IP addresses. With multiple services/API hosted, the client application like Browser Clients or Mobile Clients need to know host addresses of each of the service. This increases the complexity of the entire solution. E.g. if any of the service is changed with respect to its address then client application needs to be changed this increases the maintenance cost. One of the best ways of the server-side application development is to use the API gateways. YARP is one of such solutions.

YARP provides a reverse proxy server that sits between client and backend application structure. It forwards client requests to the appropriate backend server and then returns the server's response to the client. Figure 1 shows the YARP uses in ASP.NET Core application.



Figure 1: The YARP with ASP.NET Core

 YARP provides following features:

  • Routing:
    • This is used to Directs requests to different backend servers based on predefined rules, such as URL patterns or request headers.
  • Load Balancing:
    • This is used to Distributes incoming traffic across multiple backend servers to prevent overloading a specific server. Distribution increases performance and reliability.
  • Scalability
    • YARP works by distributing traffic across multiple servers, the reverse proxy helps to scale apps to handle increasing users and higher loads. Backend servers scaled by adding or removing them without impacting the client.
  • Connection abstraction decoupling and control over URL:
    • The Inbound requests from external clients and outbound responses from the backend are independent
  • Security:
    • Internal service endpoints can be hidden from external exposure, protecting against various types of cyber attacks  
  • Caching:
    • The Frequently requested resources can be cached to reduce the load on backend servers and improve response times.
  • Versioning:
    •  Different versions of an API can be supported using different URL mappings
The Reverse Proxy handles HTTP requests as follows:

  • Receiving requests: 
    • The reverse proxy listens the HTTP request from clients on specified ports and endpoints.
  • Terminating Connections: 
    • The inbound HTTP connections are terminated at the proxy and the new connections are used for outbound requests to destinations.
  • Routing requests: 
    • Based on configuration of routing rules, the reverse proxy checks which backend server, or cluster of servers, should handle the request.
  • Forwarding requests: 
    • The reverse proxy then forwards the client request to the appropriate backend server by transforming the path and headers as per the requirements.
  • Connection pooling: 
    • The outbound connections are pooled to reduce connection overhead.
  • Processing responses: 
    • The backend server processes the request and sends a response back to the reverse proxy.
  • Returning responses: 
    • The reverse proxy then receives the response from the backend server and forwards it back to the client.
To use the YARP in the ASP.NET Core application we need to install following packages in the all projects:

  • Yarp.ReverseProxy
  • Yarp.Telemetry.Consumption
For the current article, I have created 3 ASP.NET Core APIs targeted to .NET 9. I have used Visual Studio 2022. 

Step 1: Open Visual Studio 2022 and create a Blank Solution named Core_YARP. In this solution, add a new class library project named CommonEntities. In this project add class files named Product.cs and Customer.cs we code shown in Listing 1.



public class Customer
{
    public int CustomerId { get; set; }
    public string? CustomerName { get; set; }
    public string? Email { get; set; }

}

public class Product
{
    public int ProductId { get; set; }
    public string? ProductName { get; set; }
}
Listing 1: The Product and Customer classes  

In the same solution, add a new class library project named DataStore. In this project add class files named Customers.cs and Products.cs with code as shown in Listing 2.


public class Customers : List<Customer>
{
        public Customers()
        {
            Add(new Customer() {CustomerId=101,CustomerName="Customer 1",Email="c1@cust.com" });
            Add(new Customer() { CustomerId = 102, CustomerName = "Customer 2", Email = "c2@cust.com" });
            Add(new Customer() { CustomerId = 103, CustomerName = "Customer 3", Email = "c3@cust.com" });
            Add(new Customer() { CustomerId = 104, CustomerName = "Customer 4", Email = "c4@cust.com" });
            Add(new Customer() { CustomerId = 104, CustomerName = "Customer 4", Email = "c4@cust.com" });
        }
}

public class Products : List<Product>
{
        public Products()
        {
            Add(new Product { ProductId = 1, ProductName = "Product 1" });
            Add(new Product { ProductId = 2, ProductName = "Product 2" });
            Add(new Product { ProductId = 3, ProductName = "Product 3" });
            Add(new Product { ProductId = 4, ProductName = "Product 4" });
            Add(new Product { ProductId = 5, ProductName = "Product 5" });
        }
}

Listing 2: The Customers and Products classes

Listing 2 shows data classes for Product and Customers. You can replace this using Data Access Layer to access the database to read data from.

Step 2: In the same solution, add a new ASP.NET Core API project named Core_CustomerService. Make sure that you choose the Minimal API approach. In this project, add references for CommonEntities and DataStore projects. Modify the Program.cs  by adding endpoint for customers as shown in Listing 3.


app.MapGet("/customers", () =>
{
   var customers = new Customers();    
    return Results.Ok(customers);
});
Listing 3: The Customers Endpoint

Make sure that, the Core_CustomerService API is exposed on port 7001 as shown in Figure 2.



Figure 2: The Endpoint 7001
 
In the same solution, add a new ASP.NET Core API projected targeted to .NET 9 and name it Core_ProductService. In this project add references for CommonEntities and DataStore project. Modify Program.cs file by adding Get and Post endpoints as shown in Listing 4.


app.MapGet("/products", () =>
{
    var products = new DataStore.Products();
    return Results.Ok(products);
});

app.MapPost("/products", (Product prd) =>
{
    var products = new DataStore.Products();
    products.Add(prd);
    return Results.Ok(products);
});
Listing 4: The ProductService

Make sure that, the Core_ProductService API is exposed on port 7002 as shown in Figure 3.



Figure 3: The Endpoint 7002

Now we have 2 API applications hosted on port 7001 and 7002 its time for creating gateway project. In the solution add a new ASP.NET Core API project named Core_Gateway. In this project, we will have to define configurations for routing to the downstream APIs. The Code in Listing 5, shows the configuration from appsettings.json file.


{
    "AllowedHosts": "*",
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning",
            "Yarp.ReverseProxy": "Debug"
        }
    },
  "ReverseProxy": {
        "Clusters": {
            "customer": {
                "Destinations": {
                    "customerService": {
                        "Address": "https://localhost:7001/"
                    }
                }
            },
            "product": {
                "Destinations": {
                    "productService": {
                        "Address": "https://localhost:7002/"
                    }
                }
            }
        },
        "Routes": {
            "customerRoute": {
                "ClusterId": "customer",
                "Match": {
                    "Path": "/customer/{**catch-all}"
                },
                "Transforms": [
                    { "PathRemovePrefix": "/customer" }
                ]
            },
            "productRoute": {
                "ClusterId": "product",
                "Match": {
                    "Path": "/product/{**catch-all}"
                },
                "Transforms": [
                    { "PathRemovePrefix": "/product" }
                ]
            }
        }
    }
}
Listing 5: The Routing configuration for YARP.

      
As shown in Listing 6, we have defined the ReverseProxy configuration. We have defined the Clusters for Destinations addresses to the target API endpoints; each cluster have the unique name. This name is called as ClusterId. The Routes defines the expression to match to the path towards destinations using ClusterId. The Transforms, transforms the match path and map the inbound request to the destination paths. E.g. customers in the inbound request will be removed before forwarding it to the destination using the Transforms configuration.

Let's expose the Gateway API on port 7000 as shown in Figure 4. 



Figure 4: The Gateway exposed on 7000

Let's modify Program.cs to define a reverse proxy as shown in Listing 6.



.................
builder.Services.AddReverseProxy()
     .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")).ConfigureHttpClient((context, handler) =>
     {
         if (handler is SocketsHttpHandler socketsHttpHandler)
         {
             socketsHttpHandler.SslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
         }
     });

.....................


// Use top-level route registrations instead of UseEndpoints  
app.MapReverseProxy();
.....
Listing 6: The ReverseProxy 
 
As shown in Listing 6, the ReverseProxy configuration is loaded from the appsettings.json file using the LoadFromConfig() method. Since we are using Https with SSL, we are using SocketHttpHandler for certificate validations. The MapReverseProxy() is the Endpoint Route Builder. This is used to add reverse proxy to the Route Table so that Inbound request will be forwarded to the destination URLs. Run all the 3 projects and test the API using Core_Gateway.http file for Product service using the URL as https://localhost:7000/product/products. This will make call to destination API as defined in routes in appsettings.json file. The request is shown in Figure 5.



Figure 5: The Request

We can test requests for each destination APIs using the YARP gateways.  

The code for this article can be downloaded from this link.  


Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach