Tools

How to Configure Ocelot + Consul for .NET Core Microservices using SignalR Without Message Bus

Here we’ll show how to make the microservices on your project scalable by increasing/decreasing the number of service instances.

October 5, 2021
Mykola Pidopryhora
$
HK$

So you have a project built as a set of microservices and you want them to be scalable by increasing/decreasing the number of service instances. Those microservices have the JWT token authorization and send direct HTTP calls to each other instead of using a message bus.

As web app developers, we had a couple of such cases and now want to share our take on it.

For that, we’ll show in this blog post how to configure the .NET web API JWT token authorization, and how to set up SignalR with authorization, the Ocelot load balancer, and the Consul discovery service.

Setting up The Load Balancer

Let’s assume we have several microservices (web API) and we want to configure them so that we could run several instances of each for scalability.

For that, we need to use some kind of load balancing, which will be configured in the following way: when one of the instances is unavailable, requests sent to it should be redirected to other instances.

One of the ways to achieve it is using the Ocelot load balancer for .NET Core.

Let’s assume we have 3 microservices:

They have authentication based on JWT token and support SignalR web socket connections.

devops ninja animation

ApiGatewayService

ApiGatewayService should contain Ocelot and Ocelot.Provider.Consul NuGet packages.

Here’s the suggested code for ApiGatewayService:

It contains Ocelot and Consul registration services with authentication based on token configuration.

appsettings.local.json

Follow this web application programming code to make it work with appsettings.local.json:

Below you will find the reasons for such configuration.

Configuring Ocelot

ocelot.local.json

We want Ocelot to be configured in a way that allows the identification service to support endpoints for authorized and non-authorized users. For that, we have to configure non-authorized endpoints for the email service with


/api/{version}/authenticate/login

/api/{version}/time/gettimezones

/api/{version}/emails/sendresetpasswordemail


All other endpoints that require authorization should be configured with the /api/{version}/{catchAll} template. Remember that you may have different routes to your endpoints, for example without {version}. Here we’re just posting an example.

Also, each microservice should be able to respond on /health check endpoint. We want to know if the service is down to prevent request balancing on it. And web socket SignalR hubs should also be configured since they require authorized access to data stored in Hubs/IdentityServiceHub.

Program.cs

The Program.cs code can look like this:

Source: EGO


Setting up IdentityService

After we’re finished with the API gateway, we can proceed to set up Indentityservice with the following web development solutions.

Program.cs

Here we use Redis to cache SignalR connections. When we have the same two or more instances of the identity service, and the client has a web socket connection to one of these instances, the next request may be balanced to another service instance which has no connection to it. That’s why we need a third place that will contain connection information for all instances. For this purpose, Redis will do perfectly.

We also have to specify how the current microservice will be registered in the DiscoveryServiceConfig Consul.

I added a configuration allowing me to make requests to other microservices via the CrossServiceCommunicatorConfig HTTP call.

As you will see, there is no direct URL  to other microservices. That’s because each instance of it may have a different URL, and we’ll retrieve it from Consul (see below).

Startup.cs

What you should check out in this code is the authentication configuration, SignalR and Consul health check, and the way HTTPS clients are set up for communication with other microservices.

It’s nothing special, but I’d like to pay additional attention to a couple more things.

First, when the app stops, it removes itself from the Consul list.

When the app starts, it registers and launches a hosted service name DiscoveryHostedService. It registers the current microservice with all its available endpoints at Consul. That’s how we get a current and up-to-date list of microservices running with their endpoints in Consul.  

DiscoveryHostedService

This is just an example initially done for the purposes of our professional web development company. It registers a microservice in Consul, checks its registration, and performs re-registration if needed. In Consul, it also saves the microservice name, URL, healthcheck endpoint, and the endpoints that can be used for communication between microservices via an HTTP client.

I placed them into the following interface in the project shared between microservices:


namespace Microservices.Infrastructure.Services.Communicators.ControllerDefinitions.EmailService
{
   public interface IEmailsController
   {
             Task<IActionResult> SendResetPasswordEmail([FromBody] ResetPasswordEmailInputParametersDto dto);
   }
}

To send requests to another microservice I am using the following base class:

BaseApiServiceCommunicator

Here, we’re getting an URL to the endpoint of the service registered in Consul. The Ocelot load balancer will forward the request to one of the instances of the required microservice no matter what the actual URL is there. Thus, you don’t have to worry about keeping the registered data up to date and miss something.

The API controller is derived from the interface presented above.

IEmailsController will provide a correct name to Consul, the service will take the name, the information about the API gateway, combine it and get the final URL which is used in the HTTP client.

Based on the Consul health check, Ocelot will not forward requests to the services that are currently down. The environment can be configured to restart the service when it goes down so that it will re-register itself and get ready to operate again.

This way, we get the ability to send messages between microservices with multiple instances and the load balancer without a message bus. The SignalR connections are used here with the authorization performed via the JWT token.

Apart from that, you can also find here an example of configuring and using the JWT token authentication between microservices, by itself and through SignalR. In order to finish that you have to add the common [Authorize] attribute to your Hub like here:

With the following code you can configure Docker for services to run as many instances as you need:

docker-compose.dcproj

docker-compose.yml

Bottom line

The suggested solution has some limitations depending on Ocelot and Consul services. The main limitation is the Consul metadata size. Since metadata gets populated with descriptions of the API endpoints used for direct cross-service communication, there’s a limit for the maximum amount of endpoints.

Of course, this example is limited in its functionality and applicability. But it can be easily tweaked to serve the purposes of other web and app development companies. For more information, refer to the documentation on Consul and Ocelot.

LIKE THIS ARTICLE? Help us SPREAD THE WORD.

More Articles

Back to blog