Skip to content
Katabench
Try free
7 min read The Katabench team

Aspire Service Discovery: Names, Ports, Replicas

Learn how Aspire service discovery turns logical names into endpoints with WithReference, HttpClient resolution, health checks, and replica load balancing.

Your gateway calls a catalog at http://localhost:5147. The URL works until another developer has that port, the AppHost assigns a different one, the service moves into a container, or three catalog replicas replace one process. The business dependency is still "catalog," but the code knows a fact about today's topology.

Aspire service discovery separates those two ideas. Application code keeps the logical name; the AppHost supplies the current endpoints to the processes that reference that resource; an HttpClient resolver turns the name into an address when the call runs. Understanding those three parts makes the feature predictable and makes its failures much easier to diagnose.

The AppHost publishes an address to a specific caller

Start with the relationship in AppHost.cs:

var builder = DistributedApplication.CreateBuilder(args);

var catalog = builder.AddProject<Projects.Catalog>("catalog")
    .WithHttpEndpoint();

builder.AddProject<Projects.Gateway>("gateway")
    .WithReference(catalog);

builder.Build().Run();

AddProject(..., "catalog") gives the resource its logical name. WithReference(catalog) does not copy that URL into every process in the application. It injects the catalog's discovery information into the gateway because the gateway declared that dependency. Another service without the reference does not receive it.

For .NET callers, an HTTP endpoint becomes configuration shaped like this after environment-variable keys are normalized:

services:catalog:http:0 = http://127.0.0.1:62143

The port is an example, not a contract. The relevant contract is the resource name catalog, the endpoint name or scheme http, and an indexed address. Aspire can write multiple addresses under the same service when the discovery provider needs them.

The current Aspire service-discovery documentation describes the same four-stage path: the AppHost declares resources, WithReference injects configuration, application code uses a logical URI, and the resolver selects the real endpoint.

1. AppHost model

gateway.WithReference(catalog)

Declares who may discover which resource.

2. Configuration

services__catalog__http__0

Carries the current endpoint into the caller.

3. Resolver

https+http://catalog

Turns the logical name into the injected address.

4. Proxy

cat-1 cat-2 cat-3

Keeps one address while replicas change.

Service discovery is a configuration contract between the model and the caller, not magic DNS.

WithReference is dependency wiring. It is not network authorization, a retry policy, or proof that the target is ready when the first request arrives.

The caller still needs a resolver

The AppHost can hand the gateway a perfect services:catalog:http:0 value while a plain HttpClient still asks the operating-system DNS resolver for a host literally named catalog. Discovery works only when the caller's HTTP pipeline knows how to read the injected configuration.

The Aspire Service Defaults template normally registers that plumbing. If an existing service does not use Service Defaults, the explicit registration is small:

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"));

The typed client holds no port and no environment-specific host. https+http://catalog tells the resolver to prefer the catalog's HTTPS endpoint and fall back to HTTP. Use https://catalog when HTTPS is mandatory or http://catalog when the service intentionally exposes only HTTP.

Do not confuse a named client with a named service. AddHttpClient("catalog-client", ...) names a DI registration. The host portion of the base address, catalog, is the discovery name and must match the resource name or reference alias supplied by the AppHost.

Named endpoints are part of the logical address

One service can expose more than one endpoint: its public API, an administrative interface, or a metrics endpoint. Aspire addresses a named endpoint with an underscore prefix before the service name:

// AppHost.cs
var catalog = builder.AddProject<Projects.Catalog>("catalog")
    .WithHttpEndpoint(name: "api")
    .WithHttpEndpoint(port: 9090, name: "admin");
// Gateway/Program.cs
builder.Services.AddHttpClient<CatalogAdminClient>(client =>
    client.BaseAddress = new Uri("http://_admin.catalog"));

The first snippet is the AppHost model, and the second is the calling service. The underscore means admin is an endpoint name, not a subdomain. If resolution fails, compare the endpoint name character for character and confirm that the caller references the resource that exposes it.

Keep management endpoints out of the default reference set when ordinary callers should not receive them. Discovery configuration is not a security boundary, but distributing fewer addresses reduces accidental coupling and makes the intended topology clearer.

Readiness and discovery solve different failures

Service discovery answers "where is catalog?" It does not answer "is catalog ready?" A process can have an allocated endpoint before migrations finish, caches warm, or required data loads. Starting the gateway as soon as the catalog process exists can turn the first seconds of every deployment into avoidable 503 responses.

Model startup readiness separately:

var catalog = builder.AddProject<Projects.Catalog>("catalog")
    .WithHttpEndpoint()
    .WithHttpHealthCheck("/health");

builder.AddProject<Projects.Gateway>("gateway")
    .WithReference(catalog)
    .WaitFor(catalog);

With the health check attached, WaitFor(catalog) holds the gateway until /health reports the catalog ready. Without a resource health check, waiting for a resource can mean waiting only for its running state. Aspire's health-check guidance distinguishes AppHost startup checks from service endpoints used by load balancers and deployment platforms.

This ordering happens once. If catalog becomes unavailable at 10 a.m., WaitFor does nothing. The runtime call still needs timeouts, cancellation, retries where safe, and a circuit breaker where a failing dependency would otherwise consume the caller. Startup orchestration and runtime resilience are complementary, not interchangeable.

One logical name can sit in front of replicas

A fixed port becomes especially brittle when one catalog process becomes three. Aspire project resources can express that topology directly:

var catalog = builder.AddProject<Projects.Catalog>("catalog")
    .WithHttpEndpoint()
    .WithHttpHealthCheck("/health")
    .WithReplicas(3);

For proxied project endpoints, Aspire starts a proxy in front of the processes and load balances requests across their random internal ports. Consumers still resolve one catalog endpoint. The official project-resource networking documentation explains why the proxy owns the stable endpoint while each replica listens on a different internal port.

That is inner-loop scale for testing application behavior, not a production autoscaler. It is useful for exposing assumptions such as in-memory session affinity, process-local idempotency records, or a cache that silently depended on every request reaching the same instance. Your deployment target still decides how production replicas are created and discovered.

Diagnose discovery from the contract outward

When a call reports "No endpoints resolved" or a connection failure, inspect the path in order:

  1. Model: does the resource name match the logical URI?
  2. Reference: did this caller use WithReference for that resource?
  3. Configuration: does the caller contain a services:<name>:<endpoint>:<index> value?
  4. Resolver: did Service Defaults or AddServiceDiscovery add discovery to this client?
  5. Endpoint: does the scheme or _named endpoint match what the resource exposes?
  6. Readiness: is the endpoint resolved but the service still warming up or failing health checks?

This order separates "the name cannot resolve" from "the resolved service refused the call." The first is model or resolver wiring. The second is availability, readiness, networking, or application behavior. Treating every socket error as discovery creates long debugging sessions around the wrong layer.

Katabench's Aspire discovery lab makes each layer observable in a running system. You start with a gateway that cannot resolve catalog, inspect the exact injected configuration, add readiness ordering, remove the fixed port, and sample one logical address until three distinct replicas have answered. The Labs overview lists the Aspire course, while the Labs guide explains how its temporary workspaces and executable checks work.

The practical rule is simple: keep names in application code, keep addresses in the environment, and prove the resolver is reading the same contract the AppHost wrote. Once that boundary is clear, ports and replica counts can change without becoming source-code changes.

Get new puzzles and .NET tips in your inbox

A short note when fresh kata land, plus the C# and performance tricks behind the grading. No spam, unsubscribe anytime.