This commit is contained in:
Ho Ngoc Hai
2026-05-23 18:37:02 +07:00
parent f15d91ee29
commit 76d75c753b
3993 changed files with 403 additions and 0 deletions

View File

@@ -0,0 +1,559 @@
# Service Discovery - Reference Examples
## Complete Implementation Examples
### 1. Complete Consul Integration
```csharp
/// <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
```csharp
/// <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
```csharp
/// <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
```csharp
/// <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
```csharp
/// <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
```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
```