Aspire Service Discovery — Wiring Services in Local Development
Use .NET Aspire's service discovery to wire microservices and dependencies in local development: AppHost project, resource references, named endpoints, and how Aspire replaces manual connection string management.
What Service Discovery Solves
Without Aspire service discovery:
appsettings.Development.json in PrescriptionService:
"Services": {
"PatientService": "https://localhost:7001",
"LabService": "https://localhost:7002",
"NotificationService": "https://localhost:7003"
}
Problems:
→ Hardcoded ports — if you change the port, update every service's config
→ Developer A runs PatientService on 7001, Developer B runs it on 7100 — config diverges
→ Adding a new service requires updating all consumers
→ No health awareness — a reference to a stopped service fails silently
With Aspire service discovery:
→ AppHost defines all services and their dependencies
→ Services discover each other by name ("patient-service"), not by port
→ Port is assigned dynamically — never hardcoded
→ Aspire Dashboard shows all service health in one placeAppHost Project
// The AppHost is the orchestration entrypoint for local development
// NuGet (AppHost): Aspire.Hosting.AppHost
var builder = DistributedApplication.CreateBuilder(args);
// Infrastructure resources
var sqlServer = builder.AddSqlServer("sqlserver")
.AddDatabase("clinical-db");
var redis = builder.AddRedis("redis");
var serviceBus = builder.AddAzureServiceBus("servicebus");
// Services — each one becomes a process managed by Aspire
var patientService = builder.AddProject<Projects.PatientService>("patient-service")
.WithReference(sqlServer) // injects ConnectionStrings__clinical-db into env
.WithReference(redis);
var prescriptionService = builder.AddProject<Projects.PrescriptionService>("prescription-service")
.WithReference(sqlServer)
.WithReference(redis)
.WithReference(patientService) // service discovery: can call "patient-service"
.WithReference(serviceBus);
var labService = builder.AddProject<Projects.LabService>("lab-service")
.WithReference(sqlServer)
.WithReference(patientService)
.WithReference(prescriptionService);
builder.Build().Run();Consuming a Service by Name
// In PrescriptionService's Program.cs:
builder.Services.AddHttpClient<IPatientServiceClient, PatientServiceClient>(
client => client.BaseAddress = new Uri("https+http://patient-service"));
// "patient-service" is the name registered in AppHost — NOT a hardcoded port
// Aspire injects the actual endpoint address via environment variables:
// services__patient-service__https__0=https://localhost:XXXX
// The HttpClient resolves this automatically via Aspire's service discovery resolver
// PatientServiceClient:
public sealed class PatientServiceClient : IPatientServiceClient
{
private readonly HttpClient _http;
public PatientServiceClient(HttpClient http) => _http = http;
public async Task<PatientSummary?> GetByIdAsync(Guid patientId, CancellationToken ct)
{
var response = await _http.GetAsync($"/api/patients/{patientId}", ct);
if (response.StatusCode == HttpStatusCode.NotFound) return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PatientSummary>(ct);
}
}Adding External Services
// Aspire can also represent external/cloud services for local development
// PostgreSQL via Docker
var postgres = builder.AddPostgres("postgres")
.AddDatabase("marten-db");
// Existing Azure resources (not spun up locally — just configured)
var existingServiceBus = builder.AddConnectionString("servicebus");
// Kafka (requires Docker)
var kafka = builder.AddKafka("kafka");
// Custom container (e.g., Seq for structured logging)
var seq = builder.AddContainer("seq", "datalust/seq")
.WithEndpoint(port: 5341, scheme: "http", name: "ingest")
.WithEndpoint(port: 8080, scheme: "http", name: "ui");
// Reference Seq from services for Serilog:
var patientService = builder.AddProject<Projects.PatientService>("patient-service")
.WithReference(seq);
// Aspire injects: services__seq__ingest__0=http://localhost:5341Named Endpoints
// A service can expose multiple endpoints (e.g., HTTP for API, gRPC for internal)
var prescriptionService = builder.AddProject<Projects.PrescriptionService>("prescription-service")
.WithEndpoint(name: "http", port: 5100, scheme: "http")
.WithEndpoint(name: "grpc", port: 5101, scheme: "https");
// Consumer references specific endpoint:
var pharmacyService = builder.AddProject<Projects.PharmacyService>("pharmacy-service")
.WithReference(prescriptionService, endpointName: "grpc");
// In PharmacyService — resolved from service discovery:
builder.Services.AddGrpcClient<PrescriptionGrpc.PrescriptionGrpcClient>(client =>
client.Address = new Uri("https+http://prescription-service/grpc"));Aspire Dashboard
The Aspire Dashboard launches automatically with the AppHost.
→ URL: http://localhost:15888
Dashboard shows:
Resources tab:
→ All services and their status (Running, Stopped, Unhealthy)
→ Port assignments (dynamically assigned by Aspire)
→ Environment variables injected into each service
Console Logs tab:
→ Structured log output from all services in one view
→ Filter by service, severity, or search term
Structured Logs tab:
→ OpenTelemetry structured logs from all services
→ Full JSON including correlation IDs, trace IDs
Traces tab:
→ Distributed traces across all services
→ End-to-end request flow: API → PrescriptionService → PatientService → Database
Metrics tab:
→ Prometheus metrics from all services
→ HTTP request rates, error rates, custom business metricsProduction issue I've seen: A team of 6 developers was building a system with 5 microservices. Each developer had their own
appsettings.Development.jsonwith port assignments. Port conflicts were resolved by each developer choosing different port ranges. Over 3 months, the team accumulated 4 different port configuration conventions, 3 broken developer environments (wrong ports after a merge), and a shared "ports spreadsheet" to track who was using what. Introducing Aspire took 2 hours — dynamic port assignment eliminated all port conflicts, and the Aspire Dashboard replaced 5 separate terminal windows showing interleaved logs.
Key Takeaway
Aspire's AppHost is the single source of truth for local development topology. Services discover each other by name (not port) — ports are dynamically assigned. Use
WithReferenceto declare service dependencies — Aspire injects endpoint addresses as environment variables. The Aspire Dashboard provides a unified view of logs, traces, metrics, and resource health for all services. Aspire eliminates the "works on my machine, but not yours" problem caused by hardcoded port configurations.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.