# Service Discovery - Reference Examples ## Complete Implementation Examples ### 1. Complete Consul Integration ```csharp /// /// EN: Complete Consul service discovery setup. /// VI: Cài đặt Consul service discovery hoàn chỉnh. /// // Extensions/ConsulExtensions.cs public static class ConsulExtensions { public static IServiceCollection AddConsulDiscovery( this IServiceCollection services, IConfiguration configuration) { var consulConfig = configuration.GetSection("Consul").Get()!; // EN: Register Consul client services.AddSingleton(new ConsulClient(cfg => { cfg.Address = new Uri(consulConfig.Address); if (!string.IsNullOrEmpty(consulConfig.Token)) { cfg.Token = consulConfig.Token; } })); // EN: Register service discovery services.AddSingleton(); // EN: Register background service for registration services.AddHostedService(); return services; } public static IHttpClientBuilder AddServiceDiscovery( this IHttpClientBuilder builder, string serviceName) { builder.AddHttpMessageHandler(sp => { var discovery = sp.GetRequiredService(); return new ServiceDiscoveryDelegatingHandler(discovery, serviceName); }); return builder; } } public class ConsulConfig { public string Address { get; set; } = "http://localhost:8500"; public string? Token { get; set; } public string ServiceName { get; set; } = default!; public string ServiceHost { get; set; } = default!; public int ServicePort { get; set; } public string[] Tags { get; set; } = Array.Empty(); } ``` ### 2. Service Discovery Interface and Implementation ```csharp /// /// EN: Service discovery abstraction. /// VI: Abstraction cho service discovery. /// public interface IServiceDiscovery { Task GetServiceAsync(string serviceName, CancellationToken ct = default); Task> GetServicesAsync(string serviceName, CancellationToken ct = default); Task RegisterAsync(ServiceRegistration registration, CancellationToken ct = default); Task DeregisterAsync(string serviceId, CancellationToken ct = default); } public record ServiceInstance( string Id, string Name, string Host, int Port, IReadOnlyDictionary Metadata) { public Uri Uri => new($"http://{Host}:{Port}"); } public record ServiceRegistration( string Name, string Host, int Port, string[] Tags, TimeSpan HealthCheckInterval); /// /// EN: Consul implementation of service discovery. /// VI: Triển khai Consul cho service discovery. /// public class ConsulServiceDiscovery : IServiceDiscovery { private readonly IConsulClient _consulClient; private readonly ILogger _logger; private readonly ConcurrentDictionary _lastCacheTime = new(); private readonly ConcurrentDictionary> _cache = new(); private readonly TimeSpan _cacheDuration = TimeSpan.FromSeconds(10); public ConsulServiceDiscovery( IConsulClient consulClient, ILogger logger) { _consulClient = consulClient; _logger = logger; } public async Task GetServiceAsync( string serviceName, CancellationToken ct = default) { var services = await GetServicesAsync(serviceName, ct); if (services.Count == 0) return null; // EN: Random selection for load balancing // VI: Chọn ngẫu nhiên để load balancing return services[Random.Shared.Next(services.Count)]; } public async Task> GetServicesAsync( string serviceName, CancellationToken ct = default) { // EN: Check cache if (_lastCacheTime.TryGetValue(serviceName, out var lastTime) && DateTime.UtcNow - lastTime < _cacheDuration && _cache.TryGetValue(serviceName, out var cached)) { return cached; } // EN: Query Consul for healthy services var result = await _consulClient.Health.Service( serviceName, tag: null, passingOnly: true, ct); var instances = result.Response .Select(s => new ServiceInstance( s.Service.ID, s.Service.Service, s.Service.Address, s.Service.Port, s.Service.Meta ?? new Dictionary())) .ToList(); // EN: Update cache _cache[serviceName] = instances; _lastCacheTime[serviceName] = DateTime.UtcNow; _logger.LogDebug( "Discovered {Count} instances for {ServiceName}", instances.Count, serviceName); return instances; } public async Task RegisterAsync( ServiceRegistration registration, CancellationToken ct = default) { var serviceId = $"{registration.Name}-{Guid.NewGuid():N}"; var consulRegistration = new AgentServiceRegistration { ID = serviceId, Name = registration.Name, Address = registration.Host, Port = registration.Port, Tags = registration.Tags, Check = new AgentServiceCheck { HTTP = $"http://{registration.Host}:{registration.Port}/health", Interval = registration.HealthCheckInterval, Timeout = TimeSpan.FromSeconds(5), DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1) } }; await _consulClient.Agent.ServiceRegister(consulRegistration, ct); _logger.LogInformation( "Registered service {ServiceName} as {ServiceId}", registration.Name, serviceId); } public async Task DeregisterAsync(string serviceId, CancellationToken ct = default) { await _consulClient.Agent.ServiceDeregister(serviceId, ct); _logger.LogInformation("Deregistered service {ServiceId}", serviceId); } } ``` ### 3. Service Discovery HTTP Handler ```csharp /// /// EN: HTTP message handler that resolves service URIs via discovery. /// VI: HTTP message handler giải quyết URIs qua discovery. /// public class ServiceDiscoveryDelegatingHandler : DelegatingHandler { private readonly IServiceDiscovery _discovery; private readonly string _serviceName; private readonly ILogger _logger; public ServiceDiscoveryDelegatingHandler( IServiceDiscovery discovery, string serviceName) { _discovery = discovery; _serviceName = serviceName; _logger = LoggerFactory.Create(b => b.AddConsole()) .CreateLogger(); } protected override async Task SendAsync( HttpRequestMessage request, CancellationToken ct) { var services = await _discovery.GetServicesAsync(_serviceName, ct); if (services.Count == 0) { throw new ServiceNotFoundException(_serviceName); } // EN: Try each healthy instance // VI: Thử từng instance healthy var shuffled = services.OrderBy(_ => Random.Shared.Next()).ToList(); Exception? lastException = null; foreach (var service in shuffled) { try { // EN: Clone request for retry using var clonedRequest = await CloneRequestAsync(request); // EN: Update request URI var builder = new UriBuilder(clonedRequest.RequestUri!) { Scheme = service.Uri.Scheme, Host = service.Uri.Host, Port = service.Uri.Port }; clonedRequest.RequestUri = builder.Uri; _logger.LogDebug( "Sending request to {Uri}", clonedRequest.RequestUri); return await base.SendAsync(clonedRequest, ct); } catch (HttpRequestException ex) { _logger.LogWarning(ex, "Request to {Service} failed, trying next instance", service.Uri); lastException = ex; } } throw new ServiceUnavailableException( _serviceName, "All service instances failed", lastException); } private static async Task CloneRequestAsync( HttpRequestMessage request) { var clone = new HttpRequestMessage(request.Method, request.RequestUri); foreach (var header in request.Headers) { clone.Headers.TryAddWithoutValidation(header.Key, header.Value); } if (request.Content != null) { var content = await request.Content.ReadAsByteArrayAsync(); clone.Content = new ByteArrayContent(content); foreach (var header in request.Content.Headers) { clone.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); } } return clone; } } public class ServiceNotFoundException : Exception { public string ServiceName { get; } public ServiceNotFoundException(string serviceName) : base($"No healthy instances found for service: {serviceName}") { ServiceName = serviceName; } } public class ServiceUnavailableException : Exception { public string ServiceName { get; } public ServiceUnavailableException( string serviceName, string message, Exception? innerException = null) : base($"Service {serviceName} unavailable: {message}", innerException) { ServiceName = serviceName; } } ``` ### 4. Kubernetes-Native Service Discovery ```csharp /// /// EN: Service discovery for Kubernetes using DNS. /// VI: Service discovery cho Kubernetes dùng DNS. /// public class KubernetesServiceDiscovery : IServiceDiscovery { private readonly IConfiguration _configuration; private readonly ILogger _logger; public KubernetesServiceDiscovery( IConfiguration configuration, ILogger logger) { _configuration = configuration; _logger = logger; } public Task GetServiceAsync( string serviceName, CancellationToken ct = default) { // EN: In Kubernetes, use DNS for service discovery // VI: Trong Kubernetes, dùng DNS cho service discovery var namespace_ = _configuration["Kubernetes:Namespace"] ?? "default"; var host = $"{serviceName}.{namespace_}.svc.cluster.local"; var port = GetServicePort(serviceName); var instance = new ServiceInstance( $"{serviceName}-k8s", serviceName, host, port, new Dictionary()); return Task.FromResult(instance); } public Task> GetServicesAsync( string serviceName, CancellationToken ct = default) { // EN: For K8s, we typically use the service DNS which load balances automatically // VI: Với K8s, ta thường dùng service DNS vốn tự động load balance var instance = GetServiceAsync(serviceName, ct).Result; return Task.FromResult>( instance != null ? new[] { instance } : Array.Empty()); } public Task RegisterAsync(ServiceRegistration registration, CancellationToken ct = default) { // EN: In Kubernetes, registration happens via Pod labels/Service selectors // VI: Trong Kubernetes, đăng ký qua Pod labels/Service selectors _logger.LogInformation( "Kubernetes service registration is automatic via Pod labels"); return Task.CompletedTask; } public Task DeregisterAsync(string serviceId, CancellationToken ct = default) { // EN: In Kubernetes, deregistration happens when Pod terminates // VI: Trong Kubernetes, hủy đăng ký khi Pod kết thúc _logger.LogInformation( "Kubernetes service deregistration is automatic when Pod terminates"); return Task.CompletedTask; } private int GetServicePort(string serviceName) { // EN: Get port from configuration or use default // VI: Lấy port từ config hoặc dùng mặc định var configKey = $"Services:{serviceName}:Port"; return _configuration.GetValue(configKey) ?? 80; } } /// /// EN: Extension to configure service discovery based on environment. /// VI: Extension cấu hình service discovery theo môi trường. /// public static class ServiceDiscoveryExtensions { public static IServiceCollection AddServiceDiscovery( this IServiceCollection services, IConfiguration configuration) { var discoveryType = configuration.GetValue("ServiceDiscovery:Type"); switch (discoveryType?.ToLower()) { case "consul": services.AddConsulDiscovery(configuration); break; case "kubernetes": default: services.AddSingleton(); break; } return services; } } ``` ### 5. Complete Usage Example ```csharp /// /// EN: Complete example using service discovery. /// VI: Ví dụ hoàn chỉnh sử dụng service discovery. /// // Program.cs var builder = WebApplication.CreateBuilder(args); // EN: Add service discovery builder.Services.AddServiceDiscovery(builder.Configuration); // EN: Add HTTP clients with discovery builder.Services.AddHttpClient() .AddServiceDiscovery("user-service") .AddStandardResilienceHandler(); builder.Services.AddHttpClient() .AddServiceDiscovery("order-service") .AddStandardResilienceHandler(); // EN: Add health checks builder.Services.AddHealthChecks() .AddCheck("service-discovery"); var app = builder.Build(); app.MapHealthChecks("/health"); app.MapControllers(); app.Run(); // HealthChecks/ServiceDiscoveryHealthCheck.cs public class ServiceDiscoveryHealthCheck : IHealthCheck { private readonly IServiceDiscovery _discovery; private readonly string[] _requiredServices = { "user-service", "order-service" }; public ServiceDiscoveryHealthCheck(IServiceDiscovery discovery) { _discovery = discovery; } public async Task CheckHealthAsync( HealthCheckContext context, CancellationToken ct = default) { var missingServices = new List(); foreach (var service in _requiredServices) { var instances = await _discovery.GetServicesAsync(service, ct); if (instances.Count == 0) { missingServices.Add(service); } } if (missingServices.Any()) { return HealthCheckResult.Unhealthy( $"Missing services: {string.Join(", ", missingServices)}"); } return HealthCheckResult.Healthy("All required services available"); } } ``` ## Docker Compose for Local Development ```yaml version: '3.8' services: consul: image: consul:1.15 ports: - "8500:8500" command: agent -dev -ui -client=0.0.0.0 user-service: build: context: ./src/UserService environment: - ASPNETCORE_ENVIRONMENT=Development - Consul__Address=http://consul:8500 - Service__Name=user-service - Service__Host=user-service - Service__Port=80 depends_on: - consul order-service: build: context: ./src/OrderService environment: - ASPNETCORE_ENVIRONMENT=Development - Consul__Address=http://consul:8500 - Service__Name=order-service - Service__Host=order-service - Service__Port=80 depends_on: - consul api-gateway: build: context: ./src/ApiGateway ports: - "5000:80" environment: - Consul__Address=http://consul:8500 depends_on: - consul - user-service - order-service ```