Skip to main content

ServiceDiscovery

46. Service Discovery

Short Introduction

Service Discovery is a mechanism that allows services in a distributed system to automatically locate and communicate with each other without hard-coded network locations. It provides dynamic registration and discovery of service instances, enabling resilient communication in microservices architectures where services can scale up/down and move between hosts.

Official Definition

Service Discovery is a key component of most distributed systems and service-oriented architectures. The service instances have dynamically assigned network locations. Moreover, the set of service instances changes dynamically because of autoscaling, failures, and upgrades.

Setup/Usage with .NET 8+ Code

Consul Service Discovery Implementation:

# Install Consul packages
dotnet add package Consul
dotnet add package Microsoft.Extensions.Hosting

Service Registration:

// Services/ConsulServiceRegistry.cs
using Consul;

public interface IServiceRegistry
{
Task RegisterServiceAsync(ServiceRegistration registration);
Task DeregisterServiceAsync(string serviceId);
Task<IEnumerable<ServiceInstance>> DiscoverServicesAsync(string serviceName);
}

public class ConsulServiceRegistry : IServiceRegistry, IDisposable
{
private readonly IConsulClient _consulClient;
private readonly ILogger<ConsulServiceRegistry> _logger;

public ConsulServiceRegistry(IConsulClient consulClient, ILogger<ConsulServiceRegistry> logger)
{
_consulClient = consulClient;
_logger = logger;
}

public async Task RegisterServiceAsync(ServiceRegistration registration)
{
var consulRegistration = new AgentServiceRegistration
{
ID = registration.ServiceId,
Name = registration.ServiceName,
Address = registration.Address,
Port = registration.Port,
Tags = registration.Tags.ToArray(),
Check = new AgentServiceCheck
{
HTTP = $"http://{registration.Address}:{registration.Port}/health",
Interval = TimeSpan.FromSeconds(30),
Timeout = TimeSpan.FromSeconds(5),
DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1)
}
};

await _consulClient.Agent.ServiceRegister(consulRegistration);
_logger.LogInformation("Service {ServiceName} registered with ID {ServiceId}",
registration.ServiceName, registration.ServiceId);
}

public async Task DeregisterServiceAsync(string serviceId)
{
await _consulClient.Agent.ServiceDeregister(serviceId);
_logger.LogInformation("Service {ServiceId} deregistered", serviceId);
}

public async Task<IEnumerable<ServiceInstance>> DiscoverServicesAsync(string serviceName)
{
var services = await _consulClient.Health.Service(serviceName, "", true);

return services.Response.Select(service => new ServiceInstance
{
ServiceId = service.Service.ID,
ServiceName = service.Service.Service,
Address = service.Service.Address,
Port = service.Service.Port,
Tags = service.Service.Tags?.ToList() ?? new List<string>(),
IsHealthy = service.Checks.All(check => check.Status == HealthStatus.Passing)
});
}

public void Dispose()
{
_consulClient?.Dispose();
}
}

// Models/ServiceRegistration.cs
public class ServiceRegistration
{
public string ServiceId { get; set; } = string.Empty;
public string ServiceName { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public int Port { get; set; }
public List<string> Tags { get; set; } = new();
}

public class ServiceInstance
{
public string ServiceId { get; set; } = string.Empty;
public string ServiceName { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public int Port { get; set; }
public List<string> Tags { get; set; } = new();
public bool IsHealthy { get; set; }
}

Program.cs Integration:

// Program.cs
using Consul;

var builder = WebApplication.CreateBuilder(args);

// Add Consul client
builder.Services.AddSingleton<IConsulClient>(provider =>
{
var consulConfig = new ConsulClientConfiguration
{
Address = new Uri(builder.Configuration["Consul:Address"] ?? "http://localhost:8500")
};
return new ConsulClient(consulConfig);
});

// Add service registry
builder.Services.AddSingleton<IServiceRegistry, ConsulServiceRegistry>();
builder.Services.AddHostedService<ServiceRegistrationService>();

// Add HTTP client factory with service discovery
builder.Services.AddHttpClient<IBookingServiceClient, BookingServiceClient>();
builder.Services.AddSingleton<IServiceDiscoveryHttpClientFactory, ServiceDiscoveryHttpClientFactory>();

var app = builder.Build();

// Configure pipeline
app.MapControllers();
app.MapHealthChecks("/health");

app.Run();

// Services/ServiceRegistrationService.cs
public class ServiceRegistrationService : BackgroundService
{
private readonly IServiceRegistry _serviceRegistry;
private readonly IConfiguration _configuration;
private readonly ILogger<ServiceRegistrationService> _logger;
private string? _serviceId;

public ServiceRegistrationService(
IServiceRegistry serviceRegistry,
IConfiguration configuration,
ILogger<ServiceRegistrationService> logger)
{
_serviceRegistry = serviceRegistry;
_configuration = configuration;
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await RegisterService();

// Keep the service alive
await Task.Delay(Timeout.Infinite, stoppingToken);
}

public override async Task StopAsync(CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(_serviceId))
{
await _serviceRegistry.DeregisterServiceAsync(_serviceId);
}
await base.StopAsync(cancellationToken);
}

private async Task RegisterService()
{
try
{
var serviceName = _configuration["Service:Name"] ?? "unknown-service";
var serviceAddress = _configuration["Service:Address"] ?? "localhost";
var servicePort = int.Parse(_configuration["Service:Port"] ?? "8080");

_serviceId = $"{serviceName}-{Environment.MachineName}-{Guid.NewGuid():N}";

var registration = new ServiceRegistration
{
ServiceId = _serviceId,
ServiceName = serviceName,
Address = serviceAddress,
Port = servicePort,
Tags = new List<string> { "v1", Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Unknown" }
};

await _serviceRegistry.RegisterServiceAsync(registration);
_logger.LogInformation("Service registered successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register service");
}
}
}

Service Discovery HTTP Client:

// Services/ServiceDiscoveryHttpClientFactory.cs
public interface IServiceDiscoveryHttpClientFactory
{
Task<HttpClient> CreateClientAsync(string serviceName);
}

public class ServiceDiscoveryHttpClientFactory : IServiceDiscoveryHttpClientFactory
{
private readonly IServiceRegistry _serviceRegistry;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<ServiceDiscoveryHttpClientFactory> _logger;
private readonly Dictionary<string, DateTime> _lastDiscovery = new();
private readonly Dictionary<string, List<ServiceInstance>> _serviceCache = new();
private readonly object _lock = new();

public ServiceDiscoveryHttpClientFactory(
IServiceRegistry serviceRegistry,
IHttpClientFactory httpClientFactory,
ILogger<ServiceDiscoveryHttpClientFactory> logger)
{
_serviceRegistry = serviceRegistry;
_httpClientFactory = httpClientFactory;
_logger = logger;
}

public async Task<HttpClient> CreateClientAsync(string serviceName)
{
var serviceInstance = await GetServiceInstanceAsync(serviceName);

var client = _httpClientFactory.CreateClient();
client.BaseAddress = new Uri($"http://{serviceInstance.Address}:{serviceInstance.Port}");
client.DefaultRequestHeaders.Add("X-Service-Discovery", "Consul");

return client;
}

private async Task<ServiceInstance> GetServiceInstanceAsync(string serviceName)
{
List<ServiceInstance> instances;

lock (_lock)
{
if (_serviceCache.TryGetValue(serviceName, out instances) &&
_lastDiscovery.TryGetValue(serviceName, out var lastDiscovery) &&
DateTime.UtcNow - lastDiscovery < TimeSpan.FromMinutes(1))
{
// Use cached instances if they're recent
}
else
{
instances = null;
}
}

if (instances == null)
{
instances = (await _serviceRegistry.DiscoverServicesAsync(serviceName))
.Where(s => s.IsHealthy)
.ToList();

lock (_lock)
{
_serviceCache[serviceName] = instances;
_lastDiscovery[serviceName] = DateTime.UtcNow;
}
}

if (!instances.Any())
{
throw new InvalidOperationException($"No healthy instances found for service: {serviceName}");
}

// Simple round-robin load balancing
var selectedInstance = instances[Random.Shared.Next(instances.Count)];

_logger.LogDebug("Selected instance {ServiceId} for service {ServiceName}",
selectedInstance.ServiceId, serviceName);

return selectedInstance;
}
}

Use Cases

  • Microservices Communication: Dynamic service-to-service communication
  • Load Balancing: Distribute requests across multiple service instances
  • Auto-scaling: Automatically discover new instances as they come online
  • Fault Tolerance: Remove unhealthy instances from service discovery
  • Blue-Green Deployments: Route traffic between different service versions
  • Multi-Environment Support: Different service configurations per environment

When to Use vs When Not to Use

Use Service Discovery when:

  • Building microservices with multiple instances
  • Services need to communicate dynamically
  • Using container orchestration (Docker, Kubernetes)
  • Implementing auto-scaling
  • Need health-based routing
  • Working with multiple environments

Consider alternatives when:

  • Simple monolithic applications
  • Fixed, well-known service endpoints
  • Small number of services with static configuration
  • Network policies restrict dynamic discovery
  • Complexity outweighs benefits

Market Alternatives & Pros/Cons

Alternatives:

  • Kubernetes DNS: Built-in service discovery for Kubernetes
  • Netflix Eureka: Service registry for resilient mid-tier load balancing
  • etcd: Distributed key-value store for service discovery
  • Apache Zookeeper: Centralized service for configuration and naming
  • AWS Cloud Map: Managed service discovery for AWS
  • Azure Service Fabric: Microsoft's service discovery solution

Pros:

  • Dynamic service location
  • Automatic health checking
  • Load balancing capabilities
  • Multi-datacenter support
  • Rich querying and filtering
  • Battle-tested in production

Cons:

  • Additional infrastructure dependency
  • Potential single point of failure
  • Network latency for discovery calls
  • Complexity in service registration/deregistration
  • Consistency challenges in distributed scenarios

Complete Runnable Sample

Complete Service Discovery Setup:

# docker-compose.yml
version: "3.8"
services:
consul:
image: consul:1.15
ports:
- "8500:8500"
command: >
consul agent -dev -client=0.0.0.0 -ui
environment:
- CONSUL_BIND_INTERFACE=eth0
networks:
- hotel-network

booking-service:
build: ./BookingService
environment:
- ASPNETCORE_ENVIRONMENT=Development
- Consul__Address=http://consul:8500
- Service__Name=booking-service
- Service__Address=booking-service
- Service__Port=8080
depends_on:
- consul
networks:
- hotel-network
deploy:
replicas: 2

room-service:
build: ./RoomService
environment:
- ASPNETCORE_ENVIRONMENT=Development
- Consul__Address=http://consul:8500
- Service__Name=room-service
- Service__Address=room-service
- Service__Port=8080
depends_on:
- consul
networks:
- hotel-network
deploy:
replicas: 3

networks:
hotel-network:
driver: bridge