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 endpointand destination, and each Destination must have fields uriand 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.


publicclassDestination
    {
        publicstring Uri { get; set; }
        publicbool RequiresAuthentication { get; set; }
        publicDestination(string uri, bool requiresAuthentication)
        {
            Uri = path;
            RequiresAuthentication = requiresAuthentication;
        }
        publicDestination(string uri)
            :this(uri, false)
        {
        }
        privateDestination()
        {
            Uri = "/";
            RequiresAuthentication = false;
        }
}

Also, it will be correct to write a method in this class SendRequest. 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 the type HttpRequestthat describes the incoming request, remove all necessary information from there and send the request to the target URI. To do this, we write an auxiliary method CreateDestinationUrithat will connect the strings with the address and the parameters of the address string (query string) from the client.


privatestringCreateDestinationUri(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 a method SendRequestthat will send a request to microservice and receive a response back.


publicasync 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.


publicclassJsonLoader
    {
        publicstatic T LoadFromFile<T>(string filePath)
        {
            using (StreamReader reader = new StreamReader(filePath))
            {
                string json = reader.ReadToEnd();
                T result = JsonConvert.DeserializeObject<T>(json);
                return result;
            }
        }
        publicstatic 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:


publicclassRoute
    {
        publicstring Endpoint { get; set; }
        public Destination Destination { get; set; }
    }

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


publicclassRouter {
    public List<Route> Routes { get; set; }
    public Destination AuthenticationService { get; set; }
    publicRouter(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 that will occur in the method RouteRequest. We need to unpack the base part of the external address (base endpoint) from the request object. For example, for the address /movies/addbase 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 also possible: leave the token in the header, then microservice that the request is intended to retrieve it from.


publicasync 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.");
            }
            returnawait 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.


Also popular now: