Service Discovery
Overview
Microsoft.Extensions.ServiceDiscovery provides a pluggable service endpoint resolution system for .NET applications. It resolves logical service names (like https+http://catalog-api) to actual network endpoints using configuration, DNS, or container orchestration. It integrates directly with IHttpClientFactory and is the built-in service discovery mechanism for .NET Aspire, but can also be used standalone in any .NET application.
NuGet Packages
dotnet add package Microsoft.Extensions.ServiceDiscovery dotnet add package Microsoft.Extensions.ServiceDiscovery.Dns # DNS SRV resolution dotnet add package Microsoft.Extensions.ServiceDiscovery.Yarp # YARP integration
Basic Setup
var builder = WebApplication.CreateBuilder(args);
// Add service discovery
builder.Services.AddServiceDiscovery();
// Apply to all HttpClient instances by default
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddServiceDiscovery();
});
var app = builder.Build();
app.Run();
Configuration-Based Resolution
Define service endpoints in appsettings.json.
{
"Services": {
"catalog-api": {
"https": ["https://catalog1.example.com", "https://catalog2.example.com"],
"http": ["http://catalog1.example.com:8080"]
},
"payment-service": {
"https": ["https://payments.internal:5001"]
},
"identity-server": {
"default": ["https://identity.example.com"]
}
}
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddServiceDiscovery();
});
// Named HttpClient uses service discovery automatically
builder.Services.AddHttpClient("catalog", client =>
{
// Resolved from Services:catalog-api in config
client.BaseAddress = new Uri("https+http://catalog-api");
});
var app = builder.Build();
app.MapGet("/products", async (IHttpClientFactory factory) =>
{
var client = factory.CreateClient("catalog");
var products = await client.GetFromJsonAsync<List<Product>>("/api/products");
return Results.Ok(products);
});
Typed HttpClient with Service Discovery
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddServiceDiscovery();
});
builder.Services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("https+http://catalog-api");
});
builder.Services.AddHttpClient<PaymentClient>(client =>
{
client.BaseAddress = new Uri("https://payment-service");
});
var app = builder.Build();
app.MapPost("/orders", async (Order order, CatalogClient catalog, PaymentClient payments) =>
{
var product = await catalog.GetProductAsync(order.ProductId);
var result = await payments.ProcessAsync(order.Total);
return Results.Created($"/orders/{order.Id}", order);
});
app.Run();
public class CatalogClient(HttpClient httpClient)
{
public async Task<Product?> GetProductAsync(string productId) =>
await httpClient.GetFromJsonAsync<Product>($"/api/products/{productId}");
public async Task<List<Product>> SearchAsync(string query) =>
await httpClient.GetFromJsonAsync<List<Product>>(
$"/api/products?q={Uri.EscapeDataString(query)}") ?? [];
}
public class PaymentClient(HttpClient httpClient)
{
public async Task<PaymentResult> ProcessAsync(decimal amount) =>
await httpClient.PostAsJsonAsync("/api/payments", new { amount })
.ContinueWith(t => t.Result.Content.ReadFromJsonAsync<PaymentResult>())
.Unwrap() ?? throw new InvalidOperationException("Payment failed");
}
URI Scheme Conventions
| Scheme | Behavior |
|---|---|
https://service-name | Resolves HTTPS endpoints only |
http://service-name | Resolves HTTP endpoints only |
https+http://service-name | Prefers HTTPS, falls back to HTTP |
http+https://service-name | Prefers HTTP, falls back to HTTPS |
// Prefer HTTPS, fall back to HTTP
builder.Services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("https+http://catalog-api");
});
// HTTPS only (fails if no HTTPS endpoint available)
builder.Services.AddHttpClient<SecureClient>(client =>
{
client.BaseAddress = new Uri("https://secure-service");
});
DNS-Based Resolution
Resolve service endpoints using DNS SRV and A/AAAA records.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServiceDiscovery()
.AddDnsSrvServiceEndpointProvider(); // Add DNS SRV resolution
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddServiceDiscovery();
});
// DNS SRV record: _http._tcp.catalog-api.example.com -> catalog1:8080, catalog2:8080
builder.Services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("http://catalog-api.example.com");
});
Per-Client Service Discovery
Apply service discovery selectively rather than globally.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServiceDiscovery();
// Only this client uses service discovery
builder.Services.AddHttpClient<InternalServiceClient>(client =>
{
client.BaseAddress = new Uri("https+http://internal-api");
})
.AddServiceDiscovery();
// This client uses a static URL (no service discovery)
builder.Services.AddHttpClient<ExternalApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.external-vendor.com");
});
Integration with Resilience
Combine service discovery with Microsoft.Extensions.Http.Resilience for retries, circuit breakers, and timeouts.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServiceDiscovery();
builder.Services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("https+http://catalog-api");
})
.AddServiceDiscovery()
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromMilliseconds(500);
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30);
});
Aspire Integration
In .NET Aspire, service discovery is configured automatically by the AppHost.
// AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api");
builder.AddProject<Projects.WebFrontend>("web")
.WithReference(catalogApi); // Automatically configures service discovery
// CatalogApi/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults(); // Includes AddServiceDiscovery()
// WebFrontend/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddHttpClient<CatalogClient>(client =>
{
// "catalog-api" is resolved automatically from Aspire's service discovery
client.BaseAddress = new Uri("https+http://catalog-api");
});
Environment Variable Configuration
Service discovery can also read endpoints from environment variables (set by Aspire or container orchestrators).
# Environment variable format services__catalog-api__https__0=https://catalog1.example.com services__catalog-api__https__1=https://catalog2.example.com services__catalog-api__http__0=http://catalog1.example.com:8080
Best Practices
- •Use
https+http://as the default URI scheme for internal services to prefer HTTPS when available while gracefully falling back to HTTP during local development where TLS certificates may not be configured. - •Apply
AddServiceDiscovery()toConfigureHttpClientDefaultsto enable service discovery globally rather than adding it to each client individually, reducing boilerplate and preventing missed clients. - •Define service endpoints in
appsettings.jsonunder theServiceskey for development and testing environments, and use environment variables (services__name__scheme__index) for production deployments set by orchestrators. - •Combine service discovery with
AddStandardResilienceHandler()fromMicrosoft.Extensions.Http.Resilienceto get retries, circuit breakers, and timeouts on resolved endpoints rather than implementing retry logic manually. - •Use typed
HttpClientclasses (e.g.,CatalogClient,PaymentClient) with constructor-injectedHttpClientrather thanIHttpClientFactory.CreateClient("name")to get compile-time safety and encapsulated API logic. - •Use
AddDnsSrvServiceEndpointProvider()for Kubernetes or Consul environments where services are registered via DNS SRV records rather than duplicating endpoints in configuration files. - •Separate internal services (using service discovery) from external APIs (using static URLs) by applying
AddServiceDiscovery()per-client rather than globally when your application calls both internal and third-party APIs. - •Let .NET Aspire handle service discovery configuration automatically via
WithReference()in the AppHost rather than manually configuring endpoints inappsettings.jsonfor Aspire-orchestrated applications. - •Keep service names consistent across AppHost references, configuration keys, and
HttpClient.BaseAddressURIs (e.g., always usecatalog-api, notCatalogAPIorcatalog_api) to prevent resolution failures. - •Test service discovery in isolation by mocking
IServiceEndpointProvideror overriding configuration values in test fixtures to point at test servers, rather than relying on real service infrastructure during unit tests.