Monday, October 28, 2024

Service Discovery in Microservices with C# and .NET

Service Discovery is a key pattern in microservice architectures, where services need to find each other dynamically. Unlike monolithic applications, where all components are contained within a single executable, microservices are distributed across multiple instances, often across different servers, virtual machines, or containers. Service discovery simplifies the way these services locate one another, allowing for load balancing, failover, and flexibility.

What is Service Discovery?

Service Discovery refers to the process by which services in a distributed system dynamically find each other. Instead of hard-coding the addresses of service instances (which can change in a dynamic environment like Kubernetes), services register themselves with a centralized service registry. Other services query this registry to find the addresses of the services they need to communicate with.

service discovery microservice pattern

Benefits:

  • Scalability: New instances of services can be added dynamically.
  • Load Balancing: Requests can be routed to different instances of the same service.
  • Fault Tolerance: Dead services can be automatically removed from the registry.

Types of Service Discovery:

Client-Side Discovery

In client-side service discovery, the client directly queries the service registry to discover service instances and choose one to send a request to. This method requires the client to implement the logic for discovery and load balancing.

Server-Side Discovery

In server-side discovery, the client makes a request to a load balancer, and the load balancer queries the service registry to find available instances. The load balancer then forwards the request to an appropriate service instance.

Service Discovery in .NET/C#

In the .NET ecosystem, there are several tools and libraries that help with service discovery. These include:

  • Consul
  • etcd
  • ZooKeeper
  • Eureka (Spring Cloud for .NET)

We'll focus on Consul, which is one of the most popular choices for service discovery and distributed configuration.

Example Using Consul

Step 1: Install Consul

Before diving into C#, we need to install and run a Consul agent. You can download Consul from Consul Download and follow the installation instructions for your operating system.

Once Consul is installed, you can start the agent by running:

consul agent -dev

This command starts a local Consul agent in development mode.Open url http://localhost:8500/ in browser , you will see below ui-

consul ui
Step 2: Setting Up the API:

Now, let’s create a .NET Core API project Service.Discovery.Demo.Api.This API will act as a service that needs to be discovered by other applications.

Next, configure the API to register with the Consul upon startup. Add the Consul client package to the project

dotnet add package Consul
Step 3: Register a Service in Consul

In the Program.cs, we'll register a service with Consul. When a service starts, it registers itself with Consul using a unique service ID and some metadata.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Consul Configuration
builder.Services.AddSingleton<IConsulClient>(p => new ConsulClient(consulConfig =>
{
    var consulHost = builder.Configuration["Consul:Host"];
    var consulPort = Convert.ToInt32(builder.Configuration["Consul:Port"]);
    consulConfig.Address = new Uri($"http://{consulHost}:{consulPort}");
}));
builder.Services.AddSingleton<IServiceDiscovery, ServiceDiscovery>();

Create a class named ServiceDiscovery that implements the IServiceDiscovery interface to handle service registration:

//IServiceDiscovery interface
public interface IServiceDiscovery
{
    Task RegisterServiceAsync(string serviceName, string serviceId, string serviceAddress, int servicePort);
    Task RegisterServiceAsync(AgentServiceRegistration serviceRegistration);

    Task DeRegisterServiceAsync(string serviceId);
}
//IServiceDiscovery Implementation
 public class ServiceDiscovery : IServiceDiscovery
 {
     private readonly IConsulClient _consulClient;

     public ServiceDiscovery(IConsulClient consulClient)
     {
         _consulClient = consulClient;
     }        

     public async Task RegisterServiceAsync(string serviceName, string serviceId, string serviceAddress, int servicePort)
     {
         var registration = new AgentServiceRegistration
         {
             ID = serviceId,
             Name = serviceName,
             Address = serviceAddress,
             Port = servicePort
         };
         await _consulClient.Agent.ServiceDeregister(serviceId);
         await _consulClient.Agent.ServiceRegister(registration);
     }

     public async Task RegisterServiceAsync(AgentServiceRegistration serviceRegistration)
     {
         await _consulClient.Agent.ServiceDeregister(serviceRegistration.ID);
         await _consulClient.Agent.ServiceRegister(serviceRegistration);
     }
     public async Task DeRegisterServiceAsync(string serviceId)
     {
         await _consulClient.Agent.ServiceDeregister(serviceId);
     }
 }

Now add the service registration logic in Program.cs file

app.MapControllers();
#region Service Registration
var discovery = app.Services.GetRequiredService<IServiceDiscovery>();
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
var serviceName = builder.Configuration["ServiceRegistration:serviceName"];
var serviceId = builder.Configuration["ServiceRegistration:serviceId"];
var serviceAddress = builder.Configuration["ServiceRegistration:servcieHost"];
var servicePort = Convert.ToInt32(builder.Configuration["ServiceRegistration:servciePort"]);

lifetime.ApplicationStarted.Register(async () =>
{
    var registration = new AgentServiceRegistration
    {
        ID = serviceId,
        Name = serviceName,
        Address = serviceAddress,
        Port = servicePort
    };
    await discovery.RegisterServiceAsync(registration);
});

lifetime.ApplicationStopping.Register(async () =>
{
    await discovery.DeRegisterServiceAsync(serviceId);
});
#endregion

In the above code we are getting host, post and other configuration values from appsettings.json file. Add the below configuration in appsettings.json file.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  /* Consul Config*/
  "Consul": {
    "Host": "localhost",
    "Port": 8500
  },
  /* Service Registration Config*/
  "ServiceRegistration": {
    "serviceName": "DemoApi",
    "serviceId": "6a5f4761-ac01-4407-a81b-831a28894f0f",
    "servcieHost": "locahost",
    "servciePort": "7087"
  }
}

With these configurations, the API will register itself with the Consul upon startup and deregister upon shutdown.

Step 4: Creating the Client Application

Next, create a console application named Service.Discovery.Demo.Client and add the Consul client package to the project.

dotnet add package Consul

In the Program.cs file, configure the Consul client to discover services:

using Consul;

using (var client = new ConsulClient(consulConfig =>
{
    consulConfig.Address = new Uri("http://localhost:8500");
}))
{
    var services = await client.Catalog.Service("DemoAPi");
    foreach (var service in services.Response)
    {
        Console.WriteLine($"Service ID: {service.ServiceID}, Address: {service.ServiceAddress}, Port: {service.ServicePort}");
    }
}
Console.ReadLine();

This code snippet retrieves all instances of the Api service registered with the Consul.

Step 5: Testing the API and Client Application:

Below is the project structure

project structure

Now run both applications using the command dotnet run. When this application starts, the Consul portal will display the registered service.

consul registered services

Below is the output of the console application

service discovery output

Conclusion

Service discovery is a crucial component in building microservices at scale. In this post, we covered the basics of service discovery, types of service discovery (client-side and server-side), and demonstrated how to use Consul in a .NET application for registering and discovering services.

service discovery example in .net

Happy coding!! 😊

No comments:

Post a Comment

^ Scroll to Top