Service Discovery using Eureka, Ocelot and ASP.net core

8 minute read

Introduction

Service discovery is a process in microservice architecture that enables dynamic registration and lookup of services in a network environment. It allows microservices to discover each other, establish communication, and collaborate to complete a task. This helps to improve resilience, scalability, and flexibility of microservice-based systems. In this article, I will show you how to configure service discovery with ocelot in API gateway.

Service discovery implementations within microservices architecture discovery includes both:

  • a central server (or servers) that maintain a global view of addresses.
  • clients that connect to the central server to update and retrieve addresses.

It is always hard to manage lots of micro services as well as had to maintain tens or hundreds of LB’s and DNS records. In a microservice architecture, service lifespan is measured in seconds and minutes. With microservices, addresses are added and changed constantly as new hosts are added, ports are changed, or services are terminated. The highly transient nature of microservices is again pushing the limits of today’s technologies, and we see different organizations adopting different strategies.

Fig – Real world problem (Collected from internet)

Fig – Service discovery pattern (Collected from internet)

What is Eureka Server?
Eureka is a popular, open-source service registry software used to implement the service discovery pattern in microservice architecture. It is actually Netflix OSS product, and spring cloud offers a declarative way to register and invoke services by Java annotation. It provides a centralized registry of microservices, enabling services to dynamically discover each other without hardcoding the addresses. In a typical setup, microservices register themselves with the Eureka server, providing information such as their hostname and IP address. Other microservices can then query the Eureka server to discover the endpoint of the microservice they need to communicate with. Eureka also provides some additional features such as load balancing, failover, and health checks to ensure high availability and reliability of the microservices.

Implementation

Let’s implement Service Discovery using Ocelot and asp.net core

Tools and Technologies Used

  • Visual Studio 2022
  • .NET 6.0
  • ASP.NET Core Web API
  • Visual C#
  • Eureka

Step 1: Create solution and projects

  • Create a solution name SDDemo.sln
  • Add four Web Api Projects name BFF.Web, Customer.API, Location.API, Product.API
  • Here BFF.Web is an api gateway

Step 2: Install nuget packages

  • Install following nuget packages in BFF.Web project

PM> Install-Package MMLib.SwaggerForOcelot
PM> Install-Package Ocelot
PM> Install-Package Ocelot.Provider.Eureka
PM> Install-Package Ocelot.Provider.Polly
PM> Install-Package Steeltoe.Discovery.Eureka

  • Install following nuget packages in Customer.API project
PM> Install-Package Steeltoe.Discovery.ClientCore
PM> Install-Package Steeltoe.Discovery.Eureka
  • Install following nuget packages in Location.API project
PM> Install-Package Steeltoe.Discovery.ClientCore
PM> Install-Package Steeltoe.Discovery.Eureka
  • Install following nuget packages in Product.API project
PM> Install-Package Steeltoe.Discovery.ClientCore
PM> Install-Package Steeltoe.Discovery.Eureka

Step 3: Organize Customer.API projects

  • Register Eureka and modify program.cs as follows.

Program.cs


using Steeltoe.Discovery.Client;
using Steeltoe.Discovery.Eureka;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddServiceDiscovery(o => o.UseEureka());

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

//app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

  • Create a controller name CustomerController in Controllers folder and modify get method as follows.

CustomerController.cs


using Microsoft.AspNetCore.Mvc;

// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860

namespace Customer.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CustomerController : ControllerBase
    {
        // GET: api/<CustomerController>
        [HttpGet("GetAll")]
        public IEnumerable<string> Get()
        {
            return new string[] { "Md. Mahedee Hasan", "Khaleda Islam", "Tahiya Hasan", "Humaira Hasan" };
        }

    }
}

  • Modify appsettings.json to connect to the service registry.

appsettings.json


{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },


  "Spring": {
    "Application": {
      "Name": "SERVICE.CUSTOMERAPI"
    }
  },
  "Eureka": {
    "Client": {
      "ServiceUrl": "http://localhost:8761/eureka/",
      "ValidateCertificates": false,
      "ShouldRegisterWithEureka": true
    },

    "Instance": {
      //"port": 9001,
      "NonSecurePort": 9002,
      "HostName": "localhost",
      "InstanceId": "Customer.API,Port:9002",
      "StatusPageUrlPath": "/swagger/index.html",
      "HealthCheckUrlPath": "/api/values/healthcheck"
      //"StatusPageUrlPath": "/api/values/status"
    },

    "AllowedHosts": "*"
  }

}

Step 4: Organize Location.API project

  • Register Eureka and modify program.cs as follows.

Program.cs


using Steeltoe.Discovery.Client;
using Steeltoe.Discovery.Eureka;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

//builder.Services.AddDiscoveryClient(builder.Configuration);
builder.Services.AddServiceDiscovery(o => o.UseEureka());

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

//app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

  • Create a controller name DistrictController in Controllers folder and modify get method as follows.

DistrictController.cs


using Microsoft.AspNetCore.Mvc;

// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860

namespace Location.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class DistrictController : ControllerBase
    {
        // GET: api/<DistrictController>
        [HttpGet("GetAll")]
        public IEnumerable<string> Get()
        {
            return new string[] { "Dhaka", "Chittagong", "Chandpur", "Barisal", "Noakhali" };
        }

    }
}

  • Modify appsettings.json to connect to the service registry.

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },


  "Spring": {
    "Application": {
      "Name": "SERVICE.LOCATIONAPI"
    }
  },
  "Eureka": {
    "Client": {
      ////"serviceUrl": "http://192.168.0.107:8761/eureka/",
      "ServiceUrl": "http://localhost:8761/eureka/",
      "ValidateCertificates": false,
      "ShouldRegisterWithEureka": true
    },

    "Instance": {
      "NonSecurePort": 9001,
      "HostName": "localhost",
      "InstanceId": "Location.API,Port:9001",
      "StatusPageUrlPath": "/swagger/index.html",
      "HealthCheckUrlPath": "/api/values/healthcheck"
      //"StatusPageUrlPath": "/api/values/status"
    },

    "AllowedHosts": "*"
  }

}

Step 5: Organize Product.API

  • Register Eureka and modify program.cs as follows.

Program.cs


using Steeltoe.Discovery.Client;
using Steeltoe.Discovery.Eureka;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

//builder.Services.AddDiscoveryClient(builder.Configuration);
builder.Services.AddServiceDiscovery(o => o.UseEureka());

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

//app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

  • Create a controller name ProductsController in Controllers folder and modify get method as follows.

ProductsController.cs


using Microsoft.AspNetCore.Mvc;

// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860

namespace Product.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        // GET: api/<ProductsController>
        [HttpGet("GetAll")]
        public IEnumerable<string> Get()
        {
            return new string[] { "T-Shirt", "Casual Shirt", "Socks", "Shampo", "Business Bag" };
        }
    }
}

  • Modify appsettings.json to connect to the service registry.

appsettings.json


{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",



  "Spring": {
    "Application": {
      "Name": "SERVICE.PRODUCTAPI"
    }
  },
  "Eureka": {
    "Client": {
      "ServiceUrl": "http://localhost:8761/eureka/",
      "ValidateCertificates": false,
      "ShouldRegisterWithEureka": true
    },

    "Instance": {
      //"port": 9001,
      "NonSecurePort": 9003,
      "HostName": "localhost",
      "InstanceId": "Product.API,Port:9003",
      "StatusPageUrlPath": "/swagger/index.html",
      "HealthCheckUrlPath": "/api/values/healthcheck"
      //"StatusPageUrlPath": "/api/values/status"
    }
  }
}

Step 6: Organize BFF.Web

In this stage, we are going to configure a gateway to communicate with other services using ocelot.

  • Create a folder name - Routes.dev in root directory and add the following files. ocelot.customer.api.json, ocelot.global.json, ocelot.location.api.json, ocelot.product.api.json, ocelot.SwaggerEndPoints.json in Routes.dev folder.

  • Now modify the json files as follows.

ocelot.customer.api.json


{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{everything}",
      "DownstreamScheme": "http",
      "SwaggerKey": "customer",
      "UseServiceDiscovery": true,
      "ServiceName": "SERVICE.CUSTOMERAPI",
      "UpstreamPathTemplate": "/customer/{everything}",
      "UpstreamHttpMethod": [
        "GET",
        "POST",
        "PUT",
        "DELETE"
      ],

      /*
        LeastConnection - tracks which services are dealing with requests and sends new requests to service with least existing requests. The algorythm state is not distributed across a cluster of Ocelot’s.
        RoundRobin - loops through available services and sends requests. The algorythm state is not distributed across a cluster of Ocelot’s.
        NoLoadBalancer - takes the first available service from config or service discovery.
        CookieStickySessions - uses a cookie to stick all requests to a specific server
      */

      "LoadBalancerOptions": {
        "Type": "LeastConnection"
      }

    }
  ]
}

ocelot.location.api.json

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{everything}",
      "DownstreamScheme": "http",
      "SwaggerKey": "location",
      "UseServiceDiscovery": true,
      "ServiceName": "SERVICE.LOCATIONAPI",
      "UpstreamPathTemplate": "/location/{everything}",
      "UpstreamHttpMethod": [
        "GET",
        "POST",
        "PUT",
        "DELETE"
      ],

      "LoadBalancerOptions": {
        "Type": "LeastConnection"
      }

    }
  ]
}

ocelot.product.api.json

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{everything}",
      "DownstreamScheme": "http",
      "SwaggerKey": "product",
      "UseServiceDiscovery": true,
      "ServiceName": "SERVICE.PRODUCTAPI",
      "UpstreamPathTemplate": "/product/{everything}",
      "UpstreamHttpMethod": [
        "GET",
        "POST",
        "PUT",
        "DELETE"
      ],


      /*
        LeastConnection - tracks which services are dealing with requests and sends new requests to service with least existing requests. The algorythm state is not distributed across a cluster of Ocelot’s.
        RoundRobin - loops through available services and sends requests. The algorythm state is not distributed across a cluster of Ocelot’s.
        NoLoadBalancer - takes the first available service from config or service discovery.
        CookieStickySessions - uses a cookie to stick all requests to a specific server
      */

      "LoadBalancerOptions": {
        "Type": "LeastConnection"
      }

    }
  ]
}

ocelot.global.json


{
  "GlobalConfiguration": {
    "RequestIdKey": "OcRequestId",
    "DownstreamScheme": "http",
    "UseServiceDiscovery": true,
    "ServiceDiscoveryProvider": {
      "Host": "localhost",
      "Port": 8001,
      //Type can be Consul, Eureka
      "Type": "Eureka"
    }
  }
}

ocelot.SwaggerEndPoints.json

{
  "SwaggerEndPoints": [
    {
      "Key": "bff.web",
      "TransformByOcelotConfig": false,
      "Config": [
        {
          "Name": "BFF.Web",
          "Version": "1.0",
          "Url": "http://localhost:8001/swagger/v1/swagger.json"
        }
      ]
    },
    {
      "Key": "location",
      "TransformByOcelotConfig": true,
      "Config": [
        {
          "Name": "Location.API",
          "Version": "1.0",
          "Url": "http://localhost:8001/location/swagger/v1/swagger.json"
        }
      ]
    },
    {
      "Key": "customer",
      "TransformByOcelotConfig": true,
      "Config": [
        {
          "Name": "Customer.API",
          "Version": "1.0",
          "Url": "http://localhost:8001/customer/swagger/v1/swagger.json"
        }
      ]
    },
    {
      "Key": "product",
      "TransformByOcelotConfig": true,
      "Config": [
        {
          "Name": "Product.API",
          "Version": "1.0",
          "Url": "http://localhost:8001/product/swagger/v1/swagger.json"
        }
      ]
    }
  ]
}

  • Add AlterUpstream class in Config folder

AlterUpstream.cs


using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace BFF.Web.Config
{
    public class AlterUpstream
    {
        public static string AlterUpstreamSwaggerJson(HttpContext context, string swaggerJson)
        {
            var swagger = JObject.Parse(swaggerJson);
            // ... alter upstream json
            return swagger.ToString(Formatting.Indented);
        }
    }
}

  • Modify Program.cs file as follows

Program.cs


using BFF.Web.Config;
using MMLib.SwaggerForOcelot.DependencyInjection;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Eureka;
using Ocelot.Provider.Polly;
using Steeltoe.Discovery.Client;
using Steeltoe.Discovery.Eureka;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

var routes = "";
#if DEBUG
routes = "Routes.dev";
#else
routes = "Routes.prod";
#endif
;

builder.Configuration.AddOcelotWithSwaggerSupport(options =>
{
    options.Folder = routes;
});

builder.Services.AddOcelot(builder.Configuration).AddEureka().AddPolly();

builder.Services.AddSwaggerForOcelot(builder.Configuration);

// Register service discovery - Eureka
builder.Services.AddServiceDiscovery(o => o.UseEureka());

builder.Host.ConfigureAppConfiguration((hostingContext, config) =>
{
    config.SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.local.json", optional: true, reloadOnChange: true)
    .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", optional: true)
    .AddOcelot(routes, builder.Environment)
    .AddEnvironmentVariables();
});

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    //app.UseSwaggerUI();
}



//app.UseHttpsRedirection();

app.UseAuthorization();

app.UseSwaggerForOcelotUI(options =>
{
    options.PathToSwaggerGenerator = "/swagger/docs";
    options.ReConfigureUpstreamSwaggerJson = AlterUpstream.AlterUpstreamSwaggerJson;

}).UseOcelot().Wait();

app.MapControllers();

app.Run();

  • Now modify appsettings.json for BFF.Web

Appsettings.json


{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },


  "Spring": {
    "Application": {
      "Name": "SERVICE.BFF.Web"
    }
  },
  "Eureka": {
    "Client": {
      "ServiceUrl": {
        "DefaultZone": "http://localhost:8761/eureka/"
      },
      "ValidateCertificates": false,
      "ShouldRegisterWithEureka": true
    },

    "Instance": {
      "NonSecurePort": 8001,
      "HostName": "localhost",
      "InstanceId": "BFF.Web",
      "StatusPageUrlPath": "/swagger/index.html"
    },

    "AllowedHosts": "*"
  }

}

Step 7: Run Eureka server on docker.

  • Run the following command on powershell to run eureka server
docker run --rm -it -p 8761:8761 steeltoeoss/eureka-server
  • Now open your browser and visit http://localhost:8761/

Step 8: Now run the applications

  • Run all projects at a time
  • Browse other services throw BFF.Web – In this case your are calling services by service name not host name.
  • You will see Eureka server as follows .

Source code