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
dotnet add package Microsoft.Extensions.ServiceDiscovery.Yarp
Basic Setup
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServiceDiscovery();
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();
});
builder.Services.AddHttpClient("catalog", client =>
{
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> () =>
httpClient.PostAsJsonAsync(, { amount })
.ContinueWith(t => t.Result.Content.ReadFromJsonAsync<PaymentResult>())
.Unwrap() ?? InvalidOperationException();
}
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 |
builder.Services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("https+http://catalog-api");
});
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();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddServiceDiscovery();
});
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();
builder.Services.AddHttpClient<InternalServiceClient>(client =>
{
client.BaseAddress = new Uri("https+http://internal-api");
})
.AddServiceDiscovery();
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.
var builder = DistributedApplication.CreateBuilder(args);
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api");
builder.AddProject<Projects.WebFrontend>("web")
.WithReference(catalogApi);
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddHttpClient<CatalogClient>(client =>
{
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).
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() to ConfigureHttpClientDefaults to enable service discovery globally rather than adding it to each client individually, reducing boilerplate and preventing missed clients.
- Define service endpoints in
appsettings.json under the Services key for development and testing environments, and use environment variables (services__name__scheme__index) for production deployments set by orchestrators.
- Combine service discovery with
AddStandardResilienceHandler() from Microsoft.Extensions.Http.Resilience to get retries, circuit breakers, and timeouts on resolved endpoints rather than implementing retry logic manually.
- Use typed
HttpClient classes (e.g., CatalogClient, PaymentClient) with constructor-injected HttpClient rather than IHttpClientFactory.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 in appsettings.json for Aspire-orchestrated applications.
- Keep service names consistent across AppHost references, configuration keys, and
HttpClient.BaseAddress URIs (e.g., always use catalog-api, not CatalogAPI or catalog_api) to prevent resolution failures.
- Test service discovery in isolation by mocking
IServiceEndpointProvider or overriding configuration values in test fixtures to point at test servers, rather than relying on real service infrastructure during unit tests.