Skip directly to search

Skip directly to content

 

Scalable Microservices Architecture with .NET Made Easy – a Tutorial

 
 

Architecture | Matjaz Bravc |
25 January 2022

INTRODUCTION

When working with our clients to design solutions using a microservices architecture, we often encounter the requirement for quick and easy management of the entire system and the highest possible degree of automation to avoid having to adjust individual components.

This is a real challenge, which is why we prepared a tutorial that demonstrates the simplest possible way to establish a .NET-based microservices architecture that can be quickly and very easily scaled and adapted to client requirements.

We did not want to have to make any changes to the code or settings of individual services but control the system just by orchestrating containers in Docker.

The result is a simple microservices architecture that can be easily scaled with just a few changes in container settings. The scaling of the application is handled by two open-source components: Ocelot, which is a gateway and load balancer, and HashiCorp Consul*, the identity-based network service which acts as a service discovery agent.

Such an architecture allows us to redeploy multiple instances of a single service without coordinating the deployment with other services. The redeployed service instances are automatically registered for service discovery and immediately available through the gateway. You can imagine how big a boost this is for any development team!

Of course, using a single gateway service becomes a single point of failure in our architecture, so we need to deploy at least two instances of it to have high availability, but we will leave that problem for you to play with.

THE CONSUL SERVICE

A key part of this tutorial is the use of the Consul service to dynamically discover service endpoints. Consul automatically manages a service registry, which is updated when any new instance of a service is registered and becomes available to receive traffic. Once a service is registered with Consul, it can be discovered using the standard DNS mechanism or via a custom API. This helps us to easily scale our services.

If we are running multiple instances of the same service, Consul will randomly distribute traffic across the different instances, balancing the load between them.

Consul also provides health checks on service instances registered with it. If one of the services fails its health check, the registry will recognise this condition and will avoid returning that service’s address to clients looking up the service.

SERVICE SELF-REGISTRATION

The first step is for a service instance to register itself with the service discovery service by providing its name, ID, and address. Then the gateway is able to retrieve the address of this service by querying the Consul service discovery API using the name or ID of the service. 

Bridge with Gateway

The key thing to note here is that the service instances are registered with a unique service ID in order to differentiate between the various instances of a service which are running on the same Consul service agent. Each service must have a unique ID per node, so if their names were to conflict (as in our case), then the unique IDs allow each one to be unambiguously identified.

ARCHITECTURE OF OUR TUTORIAL APPLICATION

Our tutorial uses three instances of a very simple microservice, which just returns the request URL and ID, and a single gateway microservice (Ocelot) to provide an API to external clients. All of the services, including Consul, are containerised with Docker, based on lightweight GNU/Linux distributions of their base container images.

Bridge with Gateway

IMPLEMENTATION OF OUR TUTORIAL APPLICATION

Let’s look at how we can implement self-registration in the .NET application. First, we need to read the configuration required for service discovery from environment variables that were passed through the docker-compose.override.yml file.

Bridge with Gateway

After reading the configuration required to reach the service discovery service, we can use it to register our service. The code below is implemented as a background task (i.e. a hosted service) that registers our service in Consul by overriding any previous information about the service. If the service is shutting down, it is automatically unregistered from the Consul registry.

Bridge with Gateway

Finally, we need to register our configuration and hosted service, with its Consul dependencies, to the dependency injection container. To do this, we use a simple extension method that can be shared within our services:

Bridge with Gateway

Once we have registered our services in the service discovery service, we can start implementing the API gateway.

Creating an API gateway using Ocelot

Ocelot requires that you provide a configuration file that contains a list of Routes (configuration used to map upstream requests to API endpoints) and a GlobalConfiguration (other configuration settings like QoS, rate-limiting parameters, etc.).

In the ocelot.json file below, you can see how we forward HTTP requests. We have to specify which type of load balancer we will use. In our case, this is a RoundRobin which loops through the available services and sends requests to them.

It is important to set Consul as a service discovery service in the GlobalConfiguration for the ServiceDiscoveryProvider.

Bridge with Gateway

Some of the more important ServiceDiscoveryProvider settings in the GlobalConfiguration section are as follows: 
 

  • Host – the host of Consul
  • Port – the port of Consul
  • Type Consul – means that Ocelot will get service information from Consul per request
  • Type PollConsul – means that Ocelot will poll Consul for the latest service information
  • PollingInterval – tells Ocelot how often to call Consul for changes in the service registry


After we have defined our configuration, we can start to implement an API gateway based on .NET 5 and Ocelot. Below, you can see the implementation of an Ocelot API gateway service that uses our ocelot.json configuration file and Consul as a service registry.

The Program class contains the method Main(), which is the entry point of the .NET applications. The Program class also creates the web host upon startup. 

Bridge with Gateway

The Startup class configures the application’s services and defines the middleware pipeline. In this step, it is important to include the AddConsul middleware in the pipeline using the AddConsul extension:

Bridge with Gateway

Running the services in Docker

As mentioned earlier, we will containerise all of the services, including Consul, with Docker, using lightweight GNU/Linux distributions for the base container images.

For this, we first need to set up our docker-compose.yml, the config file for Docker Compose. It allows us to deploy, combine, and configure multiple Docker containers at the same time. In our tutorial, it looks like this: 

Bridge with Gateway

Note that our services don't contain any configuration files, as we are going to use the docker-compose.override.yml file to provide configuration values. In this configuration file, you can override existing settings from docker-compose.yml or even add completely new services. In our tutorial, it looks like this:

Bridge with Gateway

Starting the containers

To execute the Docker Compose file and start the containers, open Powershell and navigate to the compose file in the root folder. Then execute the following command: docker-compose up -d -build, which starts and runs all of the containers. The -d parameter executes the command as a “detached” command. This means that the containers run in the background and don’t block your Powershell window. To check the running containers, use the command docker ps.

Bridge with Gateway

 

Consul Web UI

Consul offers a nice web user interface right out of the box. You can access it on port 8500 (http://localhost:8500). Let’s look at some of the screens.

The home page for the Consul UI services, showing all of the relevant information related to a Consul agent and web service check, is shown below.

Bridge with Gateway

Bridge with Gateway

 

Bridge with Gateway

Bridge with Gateway

From these screens, we can see that Consul is providing us with a service registry and performing regular health checks on the services we have registered with it.

Check it out

Let’s make several calls through the API gateway at http://localhost:9500/api/values. The load balancer will loop through the available service instances, forward requests to them, and return the responses that they produce:

Bridge with Gateway

Bridge with Gateway

Bridge with Gateway

You can now see the architecture in action, with the load balancer distributing the incoming requests across the available service instances.

CONCLUSION

Microservice systems are often not easy to build and maintain. However, this tutorial showed how easy it is to develop and deploy an application with a .NET microservices architecture.

As we have seen, Consul provides first-class support for service discovery, health checks, key-value storage for configuration items, and multi-data centre deployments. Ocelot can be used to provide an API gateway that communicates with the Consul service registry and retrieves service registrations, while the load balancer distributes load across a group of service instances by looping through the available services and forwarding requests to them.

Using both makes life significantly easier for developers facing such challenges. Wouldn’t you agree?

Find source code for this tutorial on Endava’s GitHub.

TAGS & TECHNOLOGIES



* Consul® is the registered trademark of HashiCorp.

Matjaz Bravc

Senior Developer

Matjaz is a Senior Software Engineer with more than 15 years of experience in designing and developing scalable applications in an Agile manner. His experience ranges across various fields and technologies (mostly Microsoft stack), and he is an expert .NET developer with strong backend C# development experience. His expertise also includes mission-critical distributed systems with a strong focus on object-oriented programming, microservice architecture, data warehouse design, and test-driven development. Matjaz likes to spend his free time away from the computer, with hobbies such as scuba diving, canoeing, standup paddling, and mountain biking.

 

Related Articles

  • 23 July 2019

    11 Things I wish I knew before working with Terraform – part 2

  • 25 June 2019

    11 Things I wish I knew before working with Terraform – part 1

  • 30 May 2019

    Microservices and Serverless Computing

  • 14 May 2019

    Edge Services

  • 28 January 2019

    Internet Scale Architecture

 

From This Author

  • 24 August 2021

    EHR to HL7 FHIR Integration: The Software Developer’s Guide – Part 3

  • 20 July 2021

    EHR to HL7 FHIR Integration: The Software Developer’s Guide – Part 2

  • 29 June 2021

    EHR to HL7 FHIR Integration: The Software Developer’s Guide – Part 1

Most Popular Articles

11 Things I wish I knew before working with Terraform – part 2
 

Architecture | Julian Alarcon | 23 July 2019

11 Things I wish I knew before working with Terraform – part 2

An R&D Project on AI in 3D Asset Creation for Games
 

AI | Radu Orghidan | 17 May 2022

An R&D Project on AI in 3D Asset Creation for Games

11 Things I wish I knew before working with Terraform – part 1
 

Architecture | Julian Alarcon | 25 June 2019

11 Things I wish I knew before working with Terraform – part 1

EHR to HL7 FHIR Integration: The Software Developer’s Guide – Part 1
 

Software Engineering | Matjaz Bravc | 29 June 2021

EHR to HL7 FHIR Integration: The Software Developer’s Guide – Part 1

Scalable Microservices Architecture with .NET Made Easy – a Tutorial
 

Architecture | Matjaz Bravc | 25 January 2022

Scalable Microservices Architecture with .NET Made Easy – a Tutorial

Elasticsearch and Apache Lucene: Fundamentals Behind the Relevance Score
 

Insights Through Data | Alveiro Garcia | 08 June 2021

Elasticsearch and Apache Lucene: Fundamentals Behind the Relevance Score

Is Data Mesh Going to Replace Centralised Repositories?
 

Insights Through Data | Adriana Calomfirescu | 26 July 2022

Is Data Mesh Going to Replace Centralised Repositories?

8 Tips for Sharing Technical Knowledge – Part 1
 

Software Engineering | Laurentiu Spilca | 12 November 2020

8 Tips for Sharing Technical Knowledge – Part 1

EHR to HL7 FHIR Integration: The Software Developer’s Guide – Part 2
 

Software Engineering | Matjaz Bravc | 20 July 2021

EHR to HL7 FHIR Integration: The Software Developer’s Guide – Part 2

 

Archive

  • 26 July 2022

    Is Data Mesh Going to Replace Centralised Repositories?

  • 09 June 2022

    A Spatial Analysis of the Covid-19 Infection and Its Determinants

  • 17 May 2022

    An R&D Project on AI in 3D Asset Creation for Games

  • 07 February 2022

    Using Two Cloud Vendors Side by Side – a Survey of Cost and Effort

  • 25 January 2022

    Scalable Microservices Architecture with .NET Made Easy – a Tutorial

  • 04 January 2022

    Create Production-Ready, Automated Deliverables Using a Build Pipeline for Games – Part 2

We are listening

How would you rate your experience with Endava so far?

We would appreciate talking to you about your feedback. Could you share with us your contact details?