Files
pos-system/microservices/.agent/skills/service-discovery/references/REFERENCE.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

17 KiB

Service Discovery - Reference Examples

Complete Implementation Examples

1. Complete Consul Integration

/// <summary>
/// EN: Complete Consul service discovery setup.
/// VI: Cài đặt Consul service discovery hoàn chỉnh.
/// </summary>

// Extensions/ConsulExtensions.cs
public static class ConsulExtensions
{
    public static IServiceCollection AddConsulDiscovery(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var consulConfig = configuration.GetSection("Consul").Get<ConsulConfig>()!;

        // EN: Register Consul client
        services.AddSingleton<IConsulClient>(new ConsulClient(cfg =>
        {
            cfg.Address = new Uri(consulConfig.Address);
            if (!string.IsNullOrEmpty(consulConfig.Token))
            {
                cfg.Token = consulConfig.Token;
            }
        }));

        // EN: Register service discovery
        services.AddSingleton<IServiceDiscovery, ConsulServiceDiscovery>();
        
        // EN: Register background service for registration
        services.AddHostedService<ConsulRegistrationHostedService>();

        return services;
    }

    public static IHttpClientBuilder AddServiceDiscovery(
        this IHttpClientBuilder builder,
        string serviceName)
    {
        builder.AddHttpMessageHandler(sp =>
        {
            var discovery = sp.GetRequiredService<IServiceDiscovery>();
            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<string>();
}

2. Service Discovery Interface and Implementation

/// <summary>
/// EN: Service discovery abstraction.
/// VI: Abstraction cho service discovery.
/// </summary>
public interface IServiceDiscovery
{
    Task<ServiceInstance?> GetServiceAsync(string serviceName, CancellationToken ct = default);
    Task<IReadOnlyList<ServiceInstance>> 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<string, string> Metadata)
{
    public Uri Uri => new($"http://{Host}:{Port}");
}

public record ServiceRegistration(
    string Name,
    string Host,
    int Port,
    string[] Tags,
    TimeSpan HealthCheckInterval);

/// <summary>
/// EN: Consul implementation of service discovery.
/// VI: Triển khai Consul cho service discovery.
/// </summary>
public class ConsulServiceDiscovery : IServiceDiscovery
{
    private readonly IConsulClient _consulClient;
    private readonly ILogger<ConsulServiceDiscovery> _logger;
    private readonly ConcurrentDictionary<string, DateTime> _lastCacheTime = new();
    private readonly ConcurrentDictionary<string, IReadOnlyList<ServiceInstance>> _cache = new();
    private readonly TimeSpan _cacheDuration = TimeSpan.FromSeconds(10);

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

    public async Task<ServiceInstance?> 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<IReadOnlyList<ServiceInstance>> 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<string, string>()))
            .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

/// <summary>
/// EN: HTTP message handler that resolves service URIs via discovery.
/// VI: HTTP message handler giải quyết URIs qua discovery.
/// </summary>
public class ServiceDiscoveryDelegatingHandler : DelegatingHandler
{
    private readonly IServiceDiscovery _discovery;
    private readonly string _serviceName;
    private readonly ILogger<ServiceDiscoveryDelegatingHandler> _logger;

    public ServiceDiscoveryDelegatingHandler(
        IServiceDiscovery discovery,
        string serviceName)
    {
        _discovery = discovery;
        _serviceName = serviceName;
        _logger = LoggerFactory.Create(b => b.AddConsole())
            .CreateLogger<ServiceDiscoveryDelegatingHandler>();
    }

    protected override async Task<HttpResponseMessage> 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<HttpRequestMessage> 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

/// <summary>
/// EN: Service discovery for Kubernetes using DNS.
/// VI: Service discovery cho Kubernetes dùng DNS.
/// </summary>
public class KubernetesServiceDiscovery : IServiceDiscovery
{
    private readonly IConfiguration _configuration;
    private readonly ILogger<KubernetesServiceDiscovery> _logger;

    public KubernetesServiceDiscovery(
        IConfiguration configuration,
        ILogger<KubernetesServiceDiscovery> logger)
    {
        _configuration = configuration;
        _logger = logger;
    }

    public Task<ServiceInstance?> 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<string, string>());

        return Task.FromResult<ServiceInstance?>(instance);
    }

    public Task<IReadOnlyList<ServiceInstance>> 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<IReadOnlyList<ServiceInstance>>(
            instance != null ? new[] { instance } : Array.Empty<ServiceInstance>());
    }

    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<int?>(configKey) ?? 80;
    }
}

/// <summary>
/// EN: Extension to configure service discovery based on environment.
/// VI: Extension cấu hình service discovery theo môi trường.
/// </summary>
public static class ServiceDiscoveryExtensions
{
    public static IServiceCollection AddServiceDiscovery(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var discoveryType = configuration.GetValue<string>("ServiceDiscovery:Type");

        switch (discoveryType?.ToLower())
        {
            case "consul":
                services.AddConsulDiscovery(configuration);
                break;
                
            case "kubernetes":
            default:
                services.AddSingleton<IServiceDiscovery, KubernetesServiceDiscovery>();
                break;
        }

        return services;
    }
}

5. Complete Usage Example

/// <summary>
/// EN: Complete example using service discovery.
/// VI: Ví dụ hoàn chỉnh sử dụng service discovery.
/// </summary>

// 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<IUserServiceClient, UserServiceClient>()
    .AddServiceDiscovery("user-service")
    .AddStandardResilienceHandler();

builder.Services.AddHttpClient<IOrderServiceClient, OrderServiceClient>()
    .AddServiceDiscovery("order-service")
    .AddStandardResilienceHandler();

// EN: Add health checks
builder.Services.AddHealthChecks()
    .AddCheck<ServiceDiscoveryHealthCheck>("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<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        var missingServices = new List<string>();

        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

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