📜 ⬆️ ⬇️

Create a simple API gateway in ASP.NET Core

Hi, Habr! I present to your attention the translation of the article " Creating a Simple API Gateway in ASP.NET Core ".


Reading time: ~ 10 minutes


In my previous article, JWT authentication for microservices in .NET , I reviewed the process of creating microservice for user authentication. This can be used to verify the identity of the user before performing any actions in other components of the system.


Microservice architecture diagram


Another vital component for the operation of the product is the API gateway - the system between the application and the backend, which, firstly, routes incoming requests to the corresponding microservice, and secondly, authorizes the user.


There are many frameworks that can be used to create an API gateway, for example, Ocelot in .NET core or Netflix Zuul in Java. However, in this article I will describe the process of creating a simple API gateway from scratch in .NET Core.


Project creation


To begin, create a new application by selecting the ASP.NET Core Web Application in the project creation window and Empty as the template.




The project will be based classes Startup and Program . For us, the most important part is the Configure method of the Startup class. Here we can process the incoming HTTP request and respond to it. Perhaps the Configure method will contain the following code:


app.Run(async (context) => { await context.Response.WriteAsync("Hello, World!"); }); 

Writing a router


So how exactly in the Configure method we will process requests, we will write the necessary logic:


 Router router = new Router("routes.json"); app.Run(async (context) => { var content = await router.RouteRequest(context.Request); await context.Response.WriteAsync(await content.Content.ReadAsStringAsync()); }); 

First we create an object of type Router . Its task is to store existing routes, validate and send requests according to routes. To make the code cleaner, we will load the routes from the JSON file.


The result is the following logic: after the request arrives at the gateway, it will be redirected to the router, which, in turn, will send it to the appropriate microservice.


Before writing the Router class, create a routes.json file. In this file we specify a list of routes, each of which will contain an external address (endpoint) and a destination address (destination). Also, we will add a flag signaling the need to authorize the user before redirecting.


Here’s what a file might look like:


 { "routes": [ { "endpoint": "/movies", "destination": { "uri": "http://localhost:8090/movies/", "requiresAuthentication": "true" } }, { "endpoint": "/songs", "destination": { "uri": "http://localhost:8091/songs/", "requiresAuthentication": "false" } } ], "authenticationService": { "uri": "http://localhost:8080/api/auth/" } } 

We create class Destination


We now know that each Route must have endpoint and destination , and each Destination must have the fields uri and requiresAuthentication .


Now we will write class Destination , remembering that. I will add two fields, two constructors and a private constructor without parameters for JSON deserialization.


 public class Destination { public string Uri { get; set; } public bool RequiresAuthentication { get; set; } public Destination(string uri, bool requiresAuthentication) { Uri = path; RequiresAuthentication = requiresAuthentication; } public Destination(string uri) :this(uri, false) { } private Destination() { Uri = "/"; RequiresAuthentication = false; } } 

Also, it will be correct to write the SendRequest method in this class. By this we show that each object of the Destination class will be responsible for sending the request. This method will accept an object of type HttpRequest , which describes the incoming request, remove all necessary information from there and send the request to the target URI. To do this, we write a helper method CreateDestinationUri , which will connect the lines with the address and the parameters of the address string (query string) from the client.


  private string CreateDestinationUri(HttpRequest request) { string requestPath = request.Path.ToString(); string queryString = request.QueryString.ToString(); string endpoint = ""; string[] endpointSplit = requestPath.Substring(1).Split('/'); if (endpointSplit.Length > 1) endpoint = endpointSplit[1]; return Uri + endpoint + queryString; } 

Now we can write the SendRequest method, which will send a request to microservice and receive a response back.


 public async Task<HttpResponseMessage> SendRequest(HttpRequest request) { string requestContent; using (Stream receiveStream = request.Body) { using (StreamReader readStream = new StreamReader(receiveStream, Encoding.UTF8)) { requestContent = readStream.ReadToEnd(); } } HttpClient client = new HttpClient(); HttpRequestMessage newRequest = new HttpRequestMessage(new HttpMethod(request.Method), CreateDestinationUri(request)); HttpResponseMessage response = await client.SendAsync(newRequest); return response; } 

Create a JSON parser.


Before writing the Router class, we need to create logic to deserialize a JSON file with routes. I will create a helper class for this, in which there will be two methods: one to create an object from the JSON file, and the other to deserialize.


 public class JsonLoader { public static T LoadFromFile<T>(string filePath) { using (StreamReader reader = new StreamReader(filePath)) { string json = reader.ReadToEnd(); T result = JsonConvert.DeserializeObject<T>(json); return result; } } public static T Deserialize<T>(object jsonObject) { return JsonConvert.DeserializeObject<T>(Convert.ToString(jsonObject)); } } 

Class Router.


The last thing we do before writing Router is to describe the route model:


  public class Route { public string Endpoint { get; set; } public Destination Destination { get; set; } } 

Now we will write the Router class, adding fields and a constructor there.


 public class Router { public List<Route> Routes { get; set; } public Destination AuthenticationService { get; set; } public Router(string routeConfigFilePath) { dynamic router = JsonLoader.LoadFromFile<dynamic>(routeConfigFilePath); Routes = JsonLoader.Deserialize<List<Route>>( Convert.ToString(router.routes) ); AuthenticationService = JsonLoader.Deserialize<Destination>( Convert.ToString(router.authenticationService) ); } } 

I use the dynamic type to read from JSON and write object properties to it.


Now everything is ready to describe the main functionality of the API gateway: user routing and authorization, which will occur in the RouteRequest method. We need to unpack the base part of the external address (base endpoint) from the request object. For example, for the address /movies/add base will be /movies/ . After that, we need to check if there is a description of this route. If so, then authorize the user and send the request, otherwise we return an error. I also created the ConstructErrorMessage class for convenience.


For authorization, I preferred the following path: we retrieve the token from the request header and send it as a request parameter. Another option is possible: leave the token in the header, then microservice, which the request is intended to, must extract it.


 public async Task<HttpResponseMessage> RouteRequest(HttpRequest request) { string path = request.Path.ToString(); string basePath = '/' + path.Split('/')[1]; Destination destination; try { destination = Routes.First(r => r.Endpoint.Equals(basePath)).Destination; } catch { return ConstructErrorMessage("The path could not be found."); } if (destination.RequiresAuthentication) { string token = request.Headers["token"]; request.Query.Append(new KeyValuePair<string, StringValues>("token", new StringValues(token))); HttpResponseMessage authResponse = await AuthenticationService.SendRequest(request); if (!authResponse.IsSuccessStatusCode) return ConstructErrorMessage("Authentication failed."); } return await destination.SendRequest(request); } private HttpResponseMessage ConstructErrorMessage(string error) { HttpResponseMessage errorMessage = new HttpResponseMessage { StatusCode = HttpStatusCode.NotFound, Content = new StringContent(error) }; return errorMessage; } 

Conclusion


Creating a basic API gateway does not require a lot of effort, but it will not provide the proper functionality. If you need a load balancer, you can look at existing frameworks or platforms that offer libraries for routing requests.


All code from this article is available in the GitHub repository.


')

Source: https://habr.com/ru/post/419393/


All Articles