The urge towards adopting microservices architecture has been a welcome trend in the software development industry. Microservices architecture has been one of the most talked-about technologies in recent times – it has been embraced by many leading organizations worldwide. Originally developed to solve the limitations of monolithic systems, microservices architecture has seen a significant increase in popularity over the last several years, mainly due to increased scalability, flexibility, and performance.
Since microservices-based applications comprise several different services, you often need a common interface or gateway to call these services so that you define and manage all concerns in one place rather than replicate them across all downstream services. This is precisely where an API Gateway comes in. This article briefly discusses the concepts around microservices architecture and how you can work with API Gateways to have a consistent way to connect to the microservices. Let's get started!
What Is Microservices Architecture?
Microservices architecture is a service-oriented architecture (SOA) variant in which an application comprises a collection of lightweight, loosely coupled, modular services. These services can be built to execute on various platforms and independently developed, tested, deployed, and managed.
Microservices architecture can replace long-running, complicated monolithic systems requiring considerable resource and management overhead. Microservice is a word that refers to a service having a limited set of functionalities rather than referring to the length of the code used to create it.
What Is an API Gateway?
When building microservices-based applications, an API Gateway is needed to have a central place where authentication, throttling, orchestration, etc., is implemented. Without an API Gateway in place, you might typically implement each of these in each service, and hence maintaining them for each service would be a daunting task at hand. An API Gateway decouples the service producer from its consumer, providing a security layer since you need not expose your microservices directly.
As soon as it receives a request, it breaks it into multiple requests (if needed) and then routes them to the appropriate downstream microservice. You can take advantage of an API Gateway to centralize, manage, and monitor the non-functional requirements of an application, orchestrate the cross-functional microservices, and reduce roundtrips. By managing requests consistently, an API Gateway can help reduce the latency and improve security.
The figure below illustrates an API Gateway used to connect to two downstream microservices named Customer and Product.
Usually, the service consumers or clients of a microservice don't communicate with it directly. Instead, an API Gateway provides a single entry point for directing traffic to various microservices, as shown in the figure above. Hence the clients don't have any direct access to the services and cannot exploit the services. If your API Gateway is behind the firewall, you can add an extra layer of protection around the attack surface.
An API Gateway pattern corresponds to two famous Gang of Four design patterns: Facade and Adapter. Like the Facade design pattern, an API Gateway provides an API to the consumers while encapsulating the internal architecture. An API Gateway enables communication and collaboration like in the Adapter design pattern, even if the interfaces are incompatible.
Why Do We Need an API Gateway?
A microservices-based application comprises many different microservices built using homogenous or heterogeneous technologies. An API Gateway provides a centralized point of entry for external consumers, regardless of the number or composition of the downstream microservices. An API Gateway can often contain an additional layer of rate-limiting and security.
Here are the main benefits of an API Gateway:
- Better isolation: An API Gateway provides isolation by preventing direct access to internal concerns. As a result, it can encapsulate service discovery, versioning, and other internal details from the service consumers or clients. Additionally, an API Gateway enables you to add more microservices or change boundaries without impacting the external consumers.
- Improved security: An API Gateway provides a security layer for your microservices that can help prevent attack vectors such as SQL Injection, Denial-of-Service (DoS), etc. You can leverage an API Gateway to authenticate users. If a particular service consumer needs data from multiple services, you need to authenticate the user just once hence reducing latency and making your authentication mechanism consistent across the application.
- Performance metrics: Since an API Gateway is a single component through which all requests and responses flow, it is a great place to collect metrics. For example, you can measure the count and execution times of the requests forwarded to the downstream microservices.
- Reduced complexity: Microservices have specific common concerns that include logging, rate-limiting, security, etc. You'll need more time to develop these concerns in each of the microservices your application is using. An API Gateway can eliminate this code duplication hence reducing the effort required to create these components.
API Gateways and reverse proxies
There is a lot of confusion around a reverse proxy and an API Gateway. While there are similarities between them, there are subtle differences between the two as well.
Reverse proxy servers typically sit behind a firewall and route requests from the client to the appropriate back-end server. A reverse proxy is a lightweight API Gateway that comprises a few basic security and monitoring capabilities. So, if you need an API Gateway with basic features, a reverse proxy server should suffice. Note that a reverse proxy is incapable of performing transformation or orchestration.
An API gateway sits between the client and a set of back-end services and provides much more extensive security and monitoring capabilities than a reverse proxy server. An API Gateway provides support for comprehensive service orchestration, transformation, and mediation. It also offers extensive support for transport security - much more than a simple proxy can provide.
Introducing Ocelot
In this article, we are going to use Ocelot API Gateway. It is a lightweight, open-source, scalable, and fast API Gateway based on .NET Core and specially designed for microservices architecture. Basically, it is a set of middleware designed to work with ASP.NET Core. It has several features such as routing, caching, security, rate limiting, etc.
The Order Processing Microservices-Based Application
Let's now put the concepts we've learned thus far into practice by implementing a concrete example. We'll build an order processing application that illustrates how an API Gateway can be used to invoke each service to retrieve customer and product data using the Customer and Product microservice, respectively.
Typically, an order processing microservices-based application comprises microservices such as Product, Customer, Order, OrderDetails, etc. In this example, we'll consider a minimalistic microservices-based application. This application will contain an API Gateway and two microservices - the Product and Customer microservice. The application would be simple so that we can focus more on building the API Gateway.
Prerequisites
To execute the code examples shown in this article, here are the minimum requirements you should have installed in your system:
- .NET 5 SDK
- Visual Studio 2019
The solution structure
The application you are going to build will comprise the following projects as part of a single Visual Studio solution:
project - This project represents the API Gateway and is responsible for getting requests from the clients and invoking the microservices.OrderProcessing
project - This project defines the classes and interfaces used to represent the customer microservice.OrderProcessing.Customer
project - This project defines the types used to represent the product microservice.OrderProcessing.Product
The Customer microservice project will comprise the following classes and interfaces:
class – This represents the customer entity class.Customer
interface – This represents the interface for the customer repository.ICustomerRepository
class – This represents the customer repository class that implements theCustomerRepository
interface.ICustomerRepository
class – This class represents the API controller for the Customer microservice.CustomerController
The Product microservice project will contain the following types:
class – This class represents the product entity.Product
interface – This represents the interface for the product repository.IProductRepository
class – This is the product repository class that implements theProductRepository
interface.IProductRepository
class – This represents the API controller class for the Product microservice.ProductController
The following picture shows how the solution structure of the completed application will look like:
Create the projects for the Order Processing application
Open a command shell and enter the following commands to create the three ASP.NET projects we need:
dotnet new web --framework "net5.0" -o OrderProcessing dotnet new webapi --framework "net5.0" -o OrderProcessing.Customer dotnet new webapi --framework "net5.0" -o OrderProcessing.Product
While the
OrderProcessing
project is an empty ASP.NET project, the other two projects are WebAPI projects. Ensure that you delete the default controller and entity classes from these two projects as we don’t need them.Create the Customer microservice
Create a new file named
Customer.cs
at the root of the OrderProcessing.Customer
project with the following code in there:// OrderProcessing.Customer/Customer.cs using System; namespace OrderProcessing.Customer { public class Customer { public Guid Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string EmailAddress { get; set; } } }
Create the CustomerRepository class
Create an interface named
ICustomerRepository
in a file named ICustomerRepository.cs
at the root of the OrderProcessing.Customer
project with the following code in there:// OrderProcessing.Customer/ICustomerRepository.cs using System.Collections.Generic; using System.Threading.Tasks; namespace OrderProcessing.Customer { public interface ICustomerRepository { public Task<List<Customer>> GetAllCustomers(); } }
Create the
CustomerRepository
class that implements the ICustomerRepository
interface at the root of the OrderProcessing.Customer
project as shown in the following code snippet:// OrderProcessing.Customer/CustomerRepository.cs using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace OrderProcessing.Customer { public class CustomerRepository : ICustomerRepository { private readonly List<Customer> customers = new List<Customer>(); public CustomerRepository() { customers.Add(new Customer() { Id = Guid.NewGuid(), FirstName = "Joydip", LastName = "Kanjilal", EmailAddress = "joydipkanjilal@yahoo.com" }); customers.Add(new Customer() { Id = Guid.NewGuid(), FirstName = "Steve", LastName = "Smith", EmailAddress = "stevesmith@yahoo.com" }); } public Task<List<Customer>> GetAllCustomers() { return Task.FromResult(customers); } } }
Create the CustomerController class
In the
Controllers
folder of the OrderProcessing.Customer
project, create an API controller named CustomerController
and replace the default code with the following:// OrderProcessing.Customer/Controllers/CustomerController.cs using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Threading.Tasks; namespace OrderProcessing.Customer.Controllers { [Route("api/[controller]")] [ApiController] public class CustomerController : ControllerBase { private readonly ICustomerRepository _customerRepository; public CustomerController(ICustomerRepository customerRepository) { _customerRepository = customerRepository; } [HttpGet] public async Task<ActionResult<List<Customer>>> GetAllCustomers() { return await _customerRepository.GetAllCustomers(); } } }
Create the Product microservice
Create a new file named
Product.cs
at the root of the OrderProcessing.Product
project with the following code in there:// OrderProcessing.Product/Product.cs using System; namespace OrderProcessing.Product { public class Product { public Guid Id { get; set; } public string Code { get; set; } public string Name { get; set; } public int Quantity_In_Stock { get; set; } public decimal Unit_Price { get; set; } } }
Create the ProductRepository class
Next, you should create a new file called
IProductRepository.cs
in the OrderProcessing.Product
project and write the following code to create the IProductRepository
interface.// OrderProcessing.Product/IProductRepository.cs using System.Collections.Generic; using System.Threading.Tasks; namespace OrderProcessing.Product { public interface IProductRepository { public Task<List<Product>> GetAllProducts(); } }
Create the
ProductRepository
class that implements the IProductRepository
interface at the root of the OrderProcessing.Product
project as shown in the following code snippet:// OrderProcessing.Product/ProductRepository.cs using System; using System.Collections.Generic; using System.Threading.Tasks; namespace OrderProcessing.Product { public class ProductRepository : IProductRepository { private readonly List<Product> products = new List<Product>(); public ProductRepository() { products.Add(new Product { Id = Guid.NewGuid(), Code = "P0001", Name = "Lenovo Laptop", Quantity_In_Stock = 15, Unit_Price = 125000 }); products.Add(new Product { Id = Guid.NewGuid(), Code = "P0002", Name = "DELL Laptop", Quantity_In_Stock = 25, Unit_Price = 135000 }); products.Add(new Product { Id = Guid.NewGuid(), Code = "P0003", Name = "HP Laptop", Quantity_In_Stock = 20, Unit_Price = 115000 }); } public Task<List<Product>> GetAllProducts() { return Task.FromResult(products); } } }
Create the ProductController class
In the
Controllers
folder of the OrderProcessing.Product
project, create an API controller named ProductController
and replace the default code with the following:// OrderProcessing.Product/ProductController.cs using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Threading.Tasks; namespace OrderProcessing.Product.Controllers { [Route("api/[controller]")] [ApiController] public class ProductController : ControllerBase { private readonly IProductRepository _productRepository; public ProductController(IProductRepository customerRepository) { _productRepository = customerRepository; } [HttpGet] public async Task<ActionResult<List<Product>>> GetAllCustomers() { return await _productRepository.GetAllProducts(); } } }
Implement the API Gateway Using Ocelot
Now that the projects have been created with the necessary files in them, let’s implement the API Gateway using Ocelot.
Before going any further, you should be aware of the terms upstream and downstream. While upstream refers to the request sent by the client to the API Gateway, downstream is related to the request that the API Gateway sends to a particular microservice.
Install the required package
To work with Ocelot, you must install it in your ASP.NET Core project. In our case, you will install Ocelot in the
OrderProcessing
project. You can do it by using the NuGet Package Manager inside Visual Studio IDE. Alternatively, you can execute the following command at the Package Manager Console window:Install-Package Ocelot
Implement routing
An Ocelot API Gateway accepts an incoming HTTP request and forwards it to a downstream service. Ocelot makes use of routes to define how a request is routed from one place to another. Add a new file named
ocelot.json
to this project with the following content in there:// OrderProcessing/Ocelot.json { "Routes":[ //Customer API{ "DownstreamPathTemplate":"/api/Customer", "DownstreamScheme":"http", "DownstreamHostAndPorts":[ { "Host":"localhost", "Port":"20057" } ], "UpstreamPathTemplate":"/Customer", "UpstreamHttpMethod":[ "GET" ] }, //Product API{ "DownstreamPathTemplate":"/api/Product", "DownstreamScheme":"http", "DownstreamHostAndPorts":[ { "Host":"localhost", "Port":"32345" } ], "UpstreamPathTemplate":"/Product", "UpstreamHttpMethod":[ "GET" ] } ] }
The above configuration specifies the downstream and upstream metadata (scheme, path, ports) for the customer and product microservices. So, while use the upstream metadata to call the endpoints specified here, the request is routed to the appropriate downstream service as specified in the downstream metadata. In other words, the downstream metadata is used to specify the internal service URL to redirect a request to when the API Gateway receives a new request.
You should add Ocelot to the service container by calling the
AddOcelot
method in the ConfigureServices
method of the Startup
class as shown below:// OrderProcessing/Startup.cs // ... existing code public void ConfigureServices(IServiceCollection services) { services.AddOcelot(Configuration); } // ... existing code
Next, you should enable Ocelot in the
Configure
method of the Startup
class by calling the UseOcelot
extension method as shown here:// OrderProcessing/Startup.cs // ... existing code public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseOcelot(); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); } // ... existing code
Run the projects
Now make sure that you've made all three projects as startup projects. To do this, follow these steps:
- In the Solution Explorer window, right-click on the solution file.
- Click "Properties".
- In the "Property Pages" window, select the "Multiple startup projects" radio button:
- Click Ok.
Press the F5 key to run the application. Now send an HTTP Get request to the following URL from Postman or any other HTTP client of your choice:
http://localhost:39469/Customer
The HTTP Get method of the Customer controller will be executed and the output will look like this:
Send an HTTP Get request from Postman to the following URL:
http://localhost:39469/Product
The request first goes to the API Gateway. Next, the API Gateway routes the request to the correct downstream microservice as specified in
ocelot.json
. The HTTP Get method named GetAllProducts
of the ProductController
will be called, and the output will look like this:Implement rate limiting
Rate limiting is a technique for controlling network traffic. It sets a limit on how many times you can perform a specific activity within a given period - for example, accessing a particular resource, logging into an account, etc. Typically, rate-limiting keeps track of the IP addresses and the time elapsed between requests. The IP address helps determine the source of a particular request.
A rate-limiting solution is adept at tracking the time elapsed between each request and the total number of requests in a particular period. If a single IP address makes excessive requests within a specific timeframe, the rate-limiting solution will reject the requests for a specified period.
In order to prevent your downstream services from being overburdened, Ocelot enables rate-limiting of upstream requests. The following configuration illustrates how you specify rate-limiting in Ocelot:
// OrderProcessing/Ocelot.json "RateLimitOptions":{ "ClientWhitelist":[ ], "EnableRateLimiting":true, "Period":"5s", "PeriodTimespan":1, "Limit":1, "HttpStatusCode":429 }
Let us now examine each of these options briefly:
setting - This is an array used to specify the clients that should not be affected by the rate-limiting.ClientWhitelist
setting - This is a boolean value,EnableRateLimiting
if you want to enable rate-limiting,true
otherwise.false
setting - This is used to specify the HTTP status code that is returned when rate limiting occurs.HttpStatusCode
setting - This specifies the duration that the rate limit is applicable, which in turn implies that if you make more requests within this duration than what is allowed, you'll need to wait for the duration specified in thePeriod
.PeriodTimespan
setting - This is used to specify the duration after which you can retry to connect to a service.PeriodTimespan
setting - This specifies the maximum number of requests that are allowed within the duration specified inLimit
.Period
Let us assume that rate limiting is applied to the Product microservice only. The updated
ocelot.json
file will now look like this:// OrderProcessing/Ocelot.json { "Routes":[ //Customer API { "DownstreamPathTemplate":"/api/Customer", "DownstreamScheme":"http", "DownstreamHostAndPorts":[ { "Host":"localhost", "Port":"20057" } ], "UpstreamPathTemplate":"/Customer", "UpstreamHttpMethod":[ "GET" ] }, //Product API { "DownstreamPathTemplate":"/api/Product", "DownstreamScheme":"http", "DownstreamHostAndPorts":[ { "Host":"localhost", "Port":"32345" } ], "RateLimitOptions":{ "ClientWhitelist":[ ], "EnableRateLimiting":true, "Period":"5s", "PeriodTimespan":1, "Limit":1 }, "UpstreamPathTemplate":"/Product", "UpstreamHttpMethod":[ "GET" ] } ] }
Now, run the application and send frequent requests (more than 1 per 5sec) and you’ll see the following error:
Implement caching
Caching is a widely popular technique used in web applications to keep data in memory so that the same data may be quickly accessed when required by the application. Ocelot provides support for basic caching. To take advantage of it, you should install the
Ocelot.Cache.CacheManager
NuGet package as shown below:Install-Package Ocelot.Cache.CacheManager
Next, you should configure caching using the following code in the
ConfigureServices
method:// OrderProcessing/Startup.cs // ... existing code ... public void ConfigureServices(IServiceCollection services) { services.AddOcelot(Configuration) .AddCacheManager(x => { x.WithDictionaryHandle(); }); } // ... existing code ...
Lastly, you should specify caching on a particular route in the route configuration using the following settings:
// OrderProcessing/Ocelot.json // ... existing settings ... "Routes":[ //Customer API{ "DownstreamPathTemplate":"/api/Customer", "DownstreamScheme":"http", "DownstreamHostAndPorts":[ { "Host":"localhost", "Port":"20057" } ], "FileCacheOptions":{ "TtlSeconds":30, "Region":"customercaching" }, "UpstreamPathTemplate":"/Customer", "UpstreamHttpMethod":[ "GET" ] } ] // ... existing settings ...
Here, we've set
TtlSeconds
to 30 seconds which implies that the cache will expire after this time has elapsed. Note that you should specify your cache configuration in the FileCacheOptions
section. The Region
setting identifies the area within the cache that will contain the data. This way you can clear that area by using the Ocelot's administration API. To test this, you can set a breakpoint on the HTTP Get method named
GetAllCustomers
in the CustomerController
class. When you execute the application and send an HTTP Get request to the endpoint, the breakpoint will be hit as usual. However, all subsequent calls to the same endpoint within 30 seconds (this is the duration we've specified) will fetch data, but the breakpoint will not be hit anymore.Implement correlation ID
Ocelot enables a client to send a request Id in the header to the server. Once this request Id is available in the middleware pipeline, you can log it along with other information. Ocelot can also forward this request Id to the downstream services if required. A correlation ID is a unique identifier attached to every request and response and used to track requests and responses in a distributed application. You can use either a request Id or a correlation ID when working with Ocelot to track requests.
The primary difference between a request Id and a correlation ID is that while the former uniquely identifies every HTTP request, the latter is a unique identifier attached to a particular request-response chain. While you can use
Request-Id
for every HTTP request, you can use X-Correlation-Id
for an event chain of requests and responses. X-Correlation-Id
is the name of the HTTP header attached to the downstream requests used to track HTTP requests that flow through multiple back-end services.Ocelot must know the URL that it is running on in order to perform certain administration configurations. This is the
BaseUrl
specified in the ocelot.json
file. Note that this URL should be the URL that your clients will see the API Gateway running on.Here's the complete source code of the
ocelot.json
file for your reference:// OrderProcessing/Ocelot.json { "Routes":[ //Customer API { "DownstreamPathTemplate":"/api/Customer", "DownstreamScheme":"http", "DownstreamHostAndPorts":[ { "Host":"localhost", "Port":"20057" } ], "FileCacheOptions":{ "TtlSeconds":30, "Region":"customercaching" }, "UpstreamPathTemplate":"/Customer", "UpstreamHttpMethod":[ "GET" ] }, //Product API { "DownstreamPathTemplate":"/api/Product", "DownstreamScheme":"http", "DownstreamHostAndPorts":[ { "Host":"localhost", "Port":"32345" } ], "RateLimitOptions":{ "ClientWhitelist":[ ], "EnableRateLimiting":true, "Period":"5s", "PeriodTimespan":1, "Limit":1 }, "UpstreamPathTemplate":"/Product", "UpstreamHttpMethod":[ "GET" ] } ], "GlobalConfiguration":{ "RequestIdKey":"X-Correlation-Id", "BaseUrl":"http://localhost:39469" } }
Conclusion
Choosing an exemplary architecture for the needs of your business is the first and foremost step in building applications that are flexible, scalable, and high performant. One of the most significant advantages of microservices architecture is its support for heterogeneous platforms and technologies.
Your API Gateway can manage concerns such as security, rate limiting, performance, and scalability. However, you should be aware of handling the complexity it brings in and the risk of a single point of failure. Besides, there is a learning curve when you're building microservices-based applications using an API Gateway. Possible performance degradation is yet another concern that you must handle.
The complete source code of the OrderProcessing application built throughout this article is available here.
Aside: Securing ASP.NET Core with Auth0
Securing ASP.NET Core applications with Auth0 is easy and brings a lot of great features to the table. With Auth0, you only have to write a few lines of code to get a solid identity management solution, single sign-on, support for social identity providers (like Facebook, GitHub, Twitter, etc.), and support for enterprise identity providers (like Active Directory, LDAP, SAML, custom, etc.).
On ASP.NET Core, you need to create an API in your Auth0 Management Dashboard and change a few things on your code. To create an API, you need to sign up for a free Auth0 account. After that, you need to go to the API section of the dashboard and click on "Create API". On the dialog shown, you can set the Name of your API as "Books", the Identifier as "http://books.mycompany.com", and leave the Signing Algorithm as "RS256".
After that, you have to add the call to
services.AddAuthentication()
in the ConfigureServices()
method of the Startup
class as follows:string authority = $"https://{Configuration["Auth0:Domain"]}/"; string audience = Configuration["Auth0:Audience"]; services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.Authority = authority; options.Audience = audience; });
In the body of the
Configure()
method of the Startup
class, you also need to add an invocation to app.UseAuthentication()
and app.UseAuthorization()
as shown below:app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
Make sure you invoke these methods in the order shown above. It is essential so that everything works properly.
Finally, add the following element to the
appsettings.json
configuration file:{ "Logging": { // ... }, "Auth0": { "Domain": "YOUR_DOMAIN", "Audience": "YOUR_AUDIENCE" } }
Note: Replace the placeholders
andYOUR_DOMAIN
with the actual values for the domain that you specified when creating your Auth0 account and the Identifier you assigned to your API.YOUR_AUDIENCE
About the author
Joydip Kanjilal
Software Architect and Advisor