Service Discovery in ConnectSoft Microservice Template¶
Purpose & Overview¶
Service Discovery in the ConnectSoft Microservice Template provides a provider-agnostic mechanism for resolving logical service names (e.g., https://catalog) into concrete endpoints at runtime. This enables clean, decoupled inter-service calls across different environments (local development, CI/CD, Kubernetes, cloud) without hard-coding hostnames or IP addresses.
Service discovery provides:
- Logical Service Names: Use logical names like
https://ordersinstead of hardcoded endpoints - Multi-Environment Support: Same code works across development, staging, and production
- Dynamic Resolution: Endpoints are resolved at runtime, supporting dynamic scaling and failover
- Provider Flexibility: Support for Configuration, DNS, and DNS-SRV providers
- Automatic Load Balancing: Multiple endpoints per service are automatically load balanced
- Integration with HttpClient: Seamless integration with
IHttpClientFactoryand HTTP clients - YARP Integration: Service discovery for API Gateway destinations
Service Discovery Philosophy
Service discovery enables microservices to find and communicate with each other dynamically, even when instances are ephemeral, scalable, or running in multiple environments. By using logical service names instead of hardcoded endpoints, services remain decoupled from infrastructure details, enabling flexible deployment and scaling strategies.
Architecture Overview¶
Service Discovery Flow¶
HTTP Request with Logical URI
↓
HttpClient (with Service Discovery Handler)
↓
Service Discovery Resolver
├── Configuration Provider (appsettings.json)
├── DNS Provider (DNS A/AAAA records)
└── DNS-SRV Provider (DNS SRV records)
↓
Endpoint Resolution
├── Multiple Endpoints → Load Balancing
└── Single Endpoint → Direct Connection
↓
HTTP Request to Resolved Endpoint
Service Discovery Integration Points¶
Application Startup
├── AddMicroserviceServiceDiscovery()
│ ├── AddServiceDiscoveryCore()
│ ├── AddConfigurationServiceEndpointProvider() (if Configuration)
│ ├── AddDnsServiceEndpointProvider() (if DNS)
│ └── AddDnsSrvServiceEndpointProvider() (if DNS-SRV)
↓
HttpClient Configuration
├── ConfigureHttpClientDefaults()
│ └── AddServiceDiscovery()
└── AddHttpClient<T>()
└── BaseAddress = new Uri("https://logical-service-name")
↓
Runtime Resolution
├── Logical URI → Concrete Endpoint
└── Request Execution
Service Registration¶
Registration in Application Startup¶
Service discovery is registered in MicroserviceRegistrationExtensions.cs:
// MicroserviceRegistrationExtensions.cs
internal static IServiceCollection AddMicroserviceServices(
this IServiceCollection services,
IConfiguration configuration,
IHostEnvironment environment)
{
// ... other registrations ...
services.AddMicroserviceServiceDiscovery();
// ... other registrations ...
}
Registration Extension¶
The AddMicroserviceServiceDiscovery() extension method:
// ServiceDiscoveryExtensions.cs
internal static IServiceCollection AddMicroserviceServiceDiscovery(
this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
if (OptionsExtensions.ServiceDiscoveryOptions.Enabled)
{
// Add Core Service Discovery with options
services.AddServiceDiscoveryCore(serviceDiscoveryOptions =>
{
serviceDiscoveryOptions.RefreshPeriod = OptionsExtensions.ServiceDiscoveryOptions.RefreshPeriod;
serviceDiscoveryOptions.AllowAllSchemes = OptionsExtensions.ServiceDiscoveryOptions.AllowAllSchemes;
serviceDiscoveryOptions.AllowedSchemes = OptionsExtensions.ServiceDiscoveryOptions.AllowedSchemes;
});
// Register provider based on configuration
if (OptionsExtensions.ServiceDiscoveryOptions.ServiceDiscoveryProvider ==
Options.ServiceDiscoveryProviderType.Configuration)
{
services.AddConfigurationServiceEndpointProvider(configureOptions =>
{
configureOptions.SectionName = OptionsExtensions.ServiceDiscoveryOptions
.ConfigurationServiceEndpointSectionName;
});
services.AddPassThroughServiceEndpointProvider();
}
else if (OptionsExtensions.ServiceDiscoveryOptions.ServiceDiscoveryProvider ==
Options.ServiceDiscoveryProviderType.Dns)
{
services.AddDnsServiceEndpointProvider(configure =>
{
configure.DefaultRefreshPeriod = OptionsExtensions.ServiceDiscoveryOptions.DnsOptions.DefaultRefreshPeriod;
configure.MinRetryPeriod = OptionsExtensions.ServiceDiscoveryOptions.DnsOptions.MinRetryPeriod;
configure.MaxRetryPeriod = OptionsExtensions.ServiceDiscoveryOptions.DnsOptions.MaxRetryPeriod;
configure.RetryBackOffFactor = OptionsExtensions.ServiceDiscoveryOptions.DnsOptions.RetryBackOffFactor;
});
}
else if (OptionsExtensions.ServiceDiscoveryOptions.ServiceDiscoveryProvider ==
Options.ServiceDiscoveryProviderType.DnsSrv)
{
services.AddDnsSrvServiceEndpointProvider(configure =>
{
configure.DefaultRefreshPeriod = OptionsExtensions.ServiceDiscoveryOptions.DnsSrvOptions.DefaultRefreshPeriod;
configure.MinRetryPeriod = OptionsExtensions.ServiceDiscoveryOptions.DnsSrvOptions.MinRetryPeriod;
configure.MaxRetryPeriod = OptionsExtensions.ServiceDiscoveryOptions.DnsSrvOptions.MaxRetryPeriod;
configure.RetryBackOffFactor = OptionsExtensions.ServiceDiscoveryOptions.DnsSrvOptions.RetryBackOffFactor;
configure.QuerySuffix = OptionsExtensions.ServiceDiscoveryOptions.DnsSrvOptions.QuerySuffix;
});
}
}
return services;
}
Configuration¶
Service Discovery Options¶
Service discovery is configured via ServiceDiscoveryOptions:
// ServiceDiscoveryOptions.cs
public sealed class ServiceDiscoveryOptions
{
public const string SectionName = "ServiceDiscovery";
[Required]
required public bool Enabled { get; set; } = true;
[RequiredIf(nameof(Enabled), true)]
[EnumDataType(typeof(ServiceDiscoveryProviderType))]
required public ServiceDiscoveryProviderType ServiceDiscoveryProvider { get; set; }
= ServiceDiscoveryProviderType.Configuration;
[RequiredIf(nameof(Enabled), true)]
required public bool AllowAllSchemes { get; set; } = true;
[RequiredIf(nameof(Enabled), true)]
public IList<string>? AllowedSchemes { get; set; } = new List<string> { "https" };
[RequiredIf(nameof(Enabled), true)]
[Range(typeof(TimeSpan), "00:00:01", "1.00:00:00")]
required public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60);
[RequiredIf(nameof(ServiceDiscoveryProvider), ServiceDiscoveryProviderType.Configuration)]
[MinLength(1)]
public string? ConfigurationServiceEndpointSectionName { get; set; } = "Services";
[RequiredIf(nameof(ServiceDiscoveryProvider), ServiceDiscoveryProviderType.Dns)]
[ValidateObjectMembers]
public DnsCommonOptions? DnsOptions { get; set; }
[RequiredIf(nameof(ServiceDiscoveryProvider), ServiceDiscoveryProviderType.DnsSrv)]
[ValidateObjectMembers]
public DnsSrvOptions? DnsSrvOptions { get; set; }
}
Configuration in appsettings.json¶
{
"ServiceDiscovery": {
"Enabled": true,
"ServiceDiscoveryProvider": "Configuration",
"AllowAllSchemes": false,
"AllowedSchemes": [ "https" ],
"RefreshPeriod": "00:01:00",
"ConfigurationServiceEndpointSectionName": "Services",
"Dns": {
"DefaultRefreshPeriod": "00:01:00",
"MinRetryPeriod": "00:00:01",
"MaxRetryPeriod": "00:00:30",
"RetryBackOffFactor": 2.0
},
"DnsSrv": {
"DefaultRefreshPeriod": "00:01:00",
"MinRetryPeriod": "00:00:01",
"MaxRetryPeriod": "00:00:30",
"RetryBackOffFactor": 2.0,
"QuerySuffix": "default.svc.cluster.local"
}
}
}
Configuration Options¶
| Option | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
true |
Enable or disable service discovery |
ServiceDiscoveryProvider |
enum |
Configuration |
Provider type: Configuration, Dns, or DnsSrv |
AllowAllSchemes |
bool |
true |
Allow all URI schemes (http, https) |
AllowedSchemes |
List<string> |
["https"] |
Allowed URI schemes when AllowAllSchemes is false |
RefreshPeriod |
TimeSpan |
00:01:00 |
Period between polling attempts for providers without change notifications |
ConfigurationServiceEndpointSectionName |
string |
"Services" |
Configuration section name for service endpoints |
Service Discovery Providers¶
Configuration Provider¶
Purpose: Resolve endpoints from configuration sources (appsettings.json, Azure App Configuration, environment variables).
Use Cases: - Local development - Testing environments - Static endpoint configurations - Azure App Configuration integration
Configuration:
{
"ServiceDiscovery": {
"Enabled": true,
"ServiceDiscoveryProvider": "Configuration",
"ConfigurationServiceEndpointSectionName": "Services"
},
"Services": {
"catalog": {
"http": [ "localhost:5107" ],
"https": [ "localhost:7243", "10.20.30.40:443" ]
},
"orders": {
"http": [ "orders.svc.cluster.local:8080" ],
"https": [ "orders.svc.cluster.local:8443" ]
},
"payment": {
"https": [ "payment-api.example.com:443" ]
}
}
}
Characteristics:
- Multiple endpoints per scheme supported
- Automatic load balancing across multiple endpoints
- Works with any IConfiguration source
- Supports dynamic refresh with Azure App Configuration
Custom Section Name:
{
"ServiceDiscovery": {
"ServiceDiscoveryProvider": "Configuration",
"ConfigurationServiceEndpointSectionName": "ServiceEndpoints"
},
"ServiceEndpoints": {
"catalog": {
"https": [ "api.example.com:443" ]
}
}
}
DNS Provider¶
Purpose: Resolve service endpoints via DNS A/AAAA record lookups.
Use Cases: - Kubernetes Services (standard services) - DNS-based service discovery - VM/host-based service registration
Configuration:
{
"ServiceDiscovery": {
"Enabled": true,
"ServiceDiscoveryProvider": "Dns",
"Dns": {
"DefaultRefreshPeriod": "00:01:00",
"MinRetryPeriod": "00:00:01",
"MaxRetryPeriod": "00:00:30",
"RetryBackOffFactor": 2.0
}
}
}
DNS Provider Options:
| Option | Type | Default | Description |
|---|---|---|---|
DefaultRefreshPeriod |
TimeSpan |
00:01:00 |
Default refresh period for DNS lookups |
MinRetryPeriod |
TimeSpan |
00:00:01 |
Minimum retry period on failure |
MaxRetryPeriod |
TimeSpan |
00:00:30 |
Maximum retry period on failure |
RetryBackOffFactor |
double |
2.0 |
Exponential backoff multiplier |
Usage:
// Logical URI automatically resolves via DNS
services.AddHttpClient<ICatalogClient, CatalogClient>(client =>
{
client.BaseAddress = new Uri("https://catalog-service.default.svc.cluster.local");
// DNS provider resolves the hostname to IP addresses
});
Kubernetes Integration:
In Kubernetes, services are automatically registered with DNS:
# Kubernetes Service
apiVersion: v1
kind: Service
metadata:
name: catalog-service
spec:
selector:
app: catalog
ports:
- port: 443
targetPort: 7279
DNS Resolution:
- Service name: catalog-service
- Full DNS name: catalog-service.default.svc.cluster.local
- Short name: catalog-service (within same namespace)
DNS-SRV Provider¶
Purpose: Resolve service endpoints via DNS SRV records, providing both hostname and port information.
Use Cases: - Kubernetes headless Services - Services with named ports - Advanced DNS-based service discovery
Configuration:
{
"ServiceDiscovery": {
"Enabled": true,
"ServiceDiscoveryProvider": "DnsSrv",
"DnsSrv": {
"DefaultRefreshPeriod": "00:01:00",
"MinRetryPeriod": "00:00:01",
"MaxRetryPeriod": "00:00:30",
"RetryBackOffFactor": 2.0,
"QuerySuffix": "default.svc.cluster.local"
}
}
}
DNS-SRV Provider Options:
| Option | Type | Default | Description |
|---|---|---|---|
DefaultRefreshPeriod |
TimeSpan |
00:01:00 |
Default refresh period for DNS SRV lookups |
MinRetryPeriod |
TimeSpan |
00:00:01 |
Minimum retry period on failure |
MaxRetryPeriod |
TimeSpan |
00:00:30 |
Maximum retry period on failure |
RetryBackOffFactor |
double |
2.0 |
Exponential backoff multiplier |
QuerySuffix |
string |
"default.svc.cluster.local" |
DNS suffix for SRV queries |
Kubernetes Headless Service:
# Headless Service with named port
apiVersion: v1
kind: Service
metadata:
name: catalog-service
spec:
clusterIP: None # Headless service
selector:
app: catalog
ports:
- name: https
port: 443
targetPort: 7279
DNS SRV Resolution:
- SRV record: _https._tcp.catalog-service.default.svc.cluster.local
- Returns: Hostname and port for each pod
- Provides direct pod-to-pod communication
HttpClient Integration¶
Service Discovery with HttpClient¶
Service discovery is automatically enabled for all HTTP clients:
// HttpClientExtensions.cs
internal static IServiceCollection AddMicroserviceHttpClient(this IServiceCollection services)
{
services.ConfigureHttpClientDefaults(http =>
{
if (OptionsExtensions.ServiceDiscoveryOptions.Enabled)
{
http.AddServiceDiscovery(); // Enable service discovery
}
});
services.AddHttpClient()
.AddHeaderPropagation();
return services;
}
Using Logical Service Names¶
HTTP Client Configuration:
// Register HTTP client with logical service name
services.AddHttpClient<ICatalogClient, CatalogClient>(client =>
{
client.BaseAddress = new Uri("https://catalog"); // Logical name, not DNS
client.Timeout = TimeSpan.FromSeconds(30);
});
Service Client Implementation:
// CatalogClient.cs
public class CatalogClient : ICatalogClient
{
private readonly HttpClient httpClient;
public CatalogClient(HttpClient httpClient)
{
this.httpClient = httpClient;
// httpClient.BaseAddress is already set to logical URI
// Service discovery resolves it at runtime
}
public async Task<CatalogItemDto> GetItemAsync(Guid itemId, CancellationToken cancellationToken = default)
{
// Request to logical URI: https://catalog/api/items/{itemId}
// Service discovery resolves to actual endpoint
var response = await this.httpClient.GetAsync(
$"/api/items/{itemId}",
cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<CatalogItemDto>(cancellationToken: cancellationToken);
}
}
Multiple Endpoints and Load Balancing¶
When multiple endpoints are configured, service discovery automatically load balances:
{
"Services": {
"catalog": {
"https": [
"catalog-service-1.example.com:443",
"catalog-service-2.example.com:443",
"catalog-service-3.example.com:443"
]
}
}
}
Load Balancing Behavior: - Requests are distributed across available endpoints - Failed endpoints are automatically excluded - Health checks ensure only healthy endpoints are used
Scheme Policy¶
Allow All Schemes:
Allows both http:// and https:// logical URIs.
Restrict to Specific Schemes:
Only allows https:// logical URIs (recommended for production).
Azure App Configuration Integration¶
Storing Service Discovery Configuration¶
Service discovery configuration and endpoints can be stored in Azure App Configuration:
// Program.cs
builder.Configuration.AddAzureAppConfiguration(options =>
{
options.Connect(connectionString)
.Select("ServiceDiscovery:*", LabelFilter.Null)
.Select("Services:*", LabelFilter.Null)
.ConfigureRefresh(refresh =>
{
refresh.Register("ServiceDiscovery:Sentinel", refreshAll: true);
refresh.SetRefreshInterval(TimeSpan.FromMinutes(5));
});
});
builder.Services.AddAzureAppConfiguration();
Configuration in Azure App Configuration:
ServiceDiscovery:Enabled = true
ServiceDiscovery:ServiceDiscoveryProvider = Configuration
ServiceDiscovery:AllowAllSchemes = false
ServiceDiscovery:AllowedSchemes:0 = https
Services:catalog:https:0 = catalog-service-1.example.com:443
Services:catalog:https:1 = catalog-service-2.example.com:443
Services:orders:https:0 = orders-service.example.com:443
Dynamic Refresh:
// Enable dynamic refresh middleware
app.UseAzureAppConfiguration();
// When "ServiceDiscovery:Sentinel" key changes, all service discovery
// configuration is refreshed without restarting the application
See Azure App Configuration for detailed information.
YARP Integration¶
Service Discovery for API Gateway¶
YARP (Yet Another Reverse Proxy) can use service discovery for destination resolution:
// Program.cs
services.AddServiceDiscovery();
services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddServiceDiscoveryDestinationResolver(); // Enable service discovery
YARP Configuration with Service Discovery:
{
"ReverseProxy": {
"Clusters": {
"catalog-cluster": {
"Destinations": {
"catalog": {
"Address": "https://catalog" // Logical service name
}
}
},
"orders-cluster": {
"Destinations": {
"orders": {
"Address": "https://orders" // Logical service name
}
}
}
},
"Routes": {
"catalog-route": {
"ClusterId": "catalog-cluster",
"Match": {
"Path": "/catalog/{**catchall}"
}
},
"orders-route": {
"ClusterId": "orders-cluster",
"Match": {
"Path": "/orders/{**catchall}"
}
}
}
}
}
Benefits: - API Gateway routes to logical service names - Service discovery resolves to actual endpoints - Automatic failover and load balancing - No hardcoded endpoints in gateway configuration
Environment Variables¶
Configuration via Environment Variables¶
Service discovery can be configured using environment variables:
# Service Discovery Options
export ServiceDiscovery__Enabled=true
export ServiceDiscovery__ServiceDiscoveryProvider=Configuration
export ServiceDiscovery__AllowAllSchemes=false
export ServiceDiscovery__AllowedSchemes__0=https
export ServiceDiscovery__RefreshPeriod=00:01:00
# Service Endpoints (Configuration Provider)
export Services__catalog__https__0=api.example.com:443
export Services__catalog__https__1=api2.example.com:443
export Services__orders__http__0=orders.svc.cluster.local:8080
Docker/Kubernetes Environment Variables:
# Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: microservice
env:
- name: ServiceDiscovery__Enabled
value: "true"
- name: ServiceDiscovery__ServiceDiscoveryProvider
value: "Dns"
- name: Services__catalog__https__0
value: "catalog-service.default.svc.cluster.local:443"
Service Discovery Patterns¶
Client-Side Discovery¶
Pattern: Client queries service registry to resolve endpoints, then connects directly.
Implementation: - Service discovery resolver queries provider - Client receives resolved endpoints - Client connects directly to service instances
Use Cases: - Direct service-to-service communication - Microservices in same cluster - Low-latency requirements
Server-Side Discovery¶
Pattern: Gateway or load balancer resolves endpoints and forwards requests.
Implementation: - YARP API Gateway uses service discovery - Gateway resolves logical names to endpoints - Gateway forwards requests to resolved endpoints
Use Cases: - API Gateway routing - External client access - Centralized routing and security
Best Practices¶
Do's¶
-
Use Logical Service Names
-
Restrict to HTTPS in Production
-
Configure Appropriate Refresh Periods
-
Use Multiple Endpoints for High Availability
-
Use DNS Provider in Kubernetes
-
Use DNS-SRV for Headless Services
Don'ts¶
-
Don't Hardcode Endpoints
-
Don't Use HTTP in Production
-
Don't Set Refresh Period Too Short
-
Don't Mix Providers Without Understanding
Troubleshooting¶
Issue: No Endpoints Resolved¶
Symptoms: HTTP requests fail with "No endpoints resolved for logical name" error.
Solutions:
- Configuration Provider:
- Verify
Servicessection exists in configuration - Check section name matches
ConfigurationServiceEndpointSectionName - Verify service name matches exactly (case-sensitive)
-
Check endpoint format:
"https": [ "host:port" ] -
DNS Provider:
- Verify DNS name exists and is resolvable
- Check DNS is accessible from pod/VM
- Verify DNS TTL settings
-
Test DNS resolution:
nslookup catalog-service.default.svc.cluster.local -
DNS-SRV Provider:
- Verify headless Service exists in Kubernetes
- Check named ports are configured
- Verify
QuerySuffixmatches cluster DNS suffix - Test SRV lookup:
dig _https._tcp.catalog-service.default.svc.cluster.local SRV
Issue: HTTP 5xx or Timeouts¶
Symptoms: Requests to resolved endpoints return 5xx errors or timeout.
Solutions: - Verify target service is healthy and running - Check network connectivity between services - Review resilience handler configuration (retries, timeouts) - Check service logs for errors - Verify endpoints are accessible from calling service
Issue: Scheme Blocked¶
Symptoms: Error indicating scheme is not allowed.
Solutions:
{
"ServiceDiscovery": {
"AllowAllSchemes": true // For development
// OR
"AllowAllSchemes": false,
"AllowedSchemes": [ "https" ] // For production
}
}
Issue: Dynamic Refresh Not Working¶
Symptoms: Changes to Azure App Configuration not reflected in running application.
Solutions:
- Verify UseAzureAppConfiguration() middleware is in pipeline
- Check sentinel key is being updated
- Verify refresh interval is appropriate
- Check Azure App Configuration connection string is valid
- Review logs for refresh errors
Issue: Load Balancing Not Working¶
Symptoms: All requests go to single endpoint despite multiple endpoints configured.
Solutions: - Verify multiple endpoints are configured correctly - Check endpoint health status - Review load balancing algorithm configuration - Verify endpoints are all accessible - Check for endpoint filtering (scheme, health)
Monitoring and Observability¶
Service Discovery Metrics¶
Monitor service discovery resolution:
// Custom metrics for service discovery
public class ServiceDiscoveryMetrics
{
private readonly Counter<int> resolutionCount;
private readonly Counter<int> resolutionFailures;
private readonly Histogram<double> resolutionDuration;
public void RecordResolution(string serviceName, bool success, TimeSpan duration)
{
if (success)
{
this.resolutionCount.Add(1, KeyValuePair.Create("service", serviceName));
}
else
{
this.resolutionFailures.Add(1, KeyValuePair.Create("service", serviceName));
}
this.resolutionDuration.Record(duration.TotalMilliseconds,
KeyValuePair.Create("service", serviceName));
}
}
Logging¶
Service discovery logs resolution activities:
// Service discovery logs
_logger.LogDebug("Resolving service: {ServiceName}", serviceName);
_logger.LogInformation("Resolved {ServiceName} to {Endpoint}", serviceName, endpoint);
_logger.LogWarning("Failed to resolve service: {ServiceName}", serviceName);
Related Documentation¶
- HTTP Client: HTTP client configuration and usage
- Azure App Configuration: Centralized configuration management
- Kubernetes: Kubernetes service discovery and DNS
- Configuration: Configuration management
- Header Propagation: Header propagation with HTTP clients
Summary¶
Service discovery in the ConnectSoft Microservice Template provides:
- ✅ Provider-Agnostic: Support for Configuration, DNS, and DNS-SRV providers
- ✅ Logical Service Names: Use logical names instead of hardcoded endpoints
- ✅ Multi-Environment: Same code works across all environments
- ✅ Automatic Load Balancing: Multiple endpoints automatically load balanced
- ✅ HttpClient Integration: Seamless integration with HTTP clients
- ✅ YARP Integration: Service discovery for API Gateway destinations
- ✅ Azure App Configuration: Dynamic configuration refresh
- ✅ Kubernetes Native: DNS-based discovery for Kubernetes
By leveraging service discovery, teams can:
- Decouple Services: Services remain independent of infrastructure details
- Scale Dynamically: Services can scale without configuration changes
- Deploy Flexibly: Same code works across environments
- Improve Reliability: Automatic failover and load balancing
- Simplify Configuration: Logical names instead of hardcoded endpoints
Service discovery is a critical component of microservices architecture, enabling services to find and communicate with each other dynamically while maintaining flexibility and reliability.