You open an Aspire project. The AppHost looks nothing like the .NET code you’re used to. No controllers, no services, no middleware. Just AddRedis(), AddProject<>(), WithReference(), and WaitFor(). Wiring, not logic.

The first time, that’s disorienting. Where does configuration come from? Why does this service wait for that one? What happens when something doesn’t start?

Claude Code is a surprisingly good guide here — and there’s a specific reason for that.


The AppHost is your architecture map

In most .NET applications, the architecture is scattered across dozens of files. Services get registered in Program.cs, configuration lives in appsettings.json, and the relationships between components have to be reconstructed from the whole.

Aspire does something different. The AppHost makes your application topology explicit in one file. Much more of the application structure becomes visible in code. Not everything disappears from configuration, but the important relationships — which service depends on which, who waits for whom — are easier to inspect. That’s exactly the kind of code Claude Code handles well.

Take the AppHost from Grouply, a multi-tenant fleet management application:

var apiService = builder
    .AddProject<Projects.Grouply_ApiService>("apiservice")
    .WithReference(postgresdb)
    .WaitFor(postgresdb)
    .WithReference(serviceBus)
    .WaitFor(serviceBus)
    .WithReference(keycloak)
    .WaitFor(keycloak)
    .WithExternalHttpEndpoints();

The difference between those two methods matters:

  • WithReference() makes the connection available: it injects the connection string or service address into the dependent service’s environment, enabling service discovery.
  • WaitFor() controls startup order: the service only starts once the dependency reaches a healthy state.

Point Claude Code at the AppHost and the related service project, then ask: “Explain what’s happening here and what goes wrong if you remove a WaitFor.” You get an explanation of startup order and the types of failures you can expect — startup races, transient connection failures, auth errors because the identity provider isn’t ready yet. The exact error depends on the dependency and your services’ retry behavior, but the class of problem is clear.

That’s not magic. Claude Code reads the dependency graph straight from the code — because Aspire made that graph explicit.


Adding a new service

The most common task in an Aspire project: adding a new service or resource and wiring it up correctly.

In Grouply, I needed a worker that listens on the vehicles topic of Azure Service Bus. The Service Bus was already configured in the AppHost via an extension method:

public static IResourceBuilder<AzureServiceBusResource> AddEventBus(
    this IDistributedApplicationBuilder builder)
{
    var resourceBuilder = builder
        .AddAzureServiceBus("servicebus")
        .RunAsEmulator(emulator =>
        {
            emulator.WithLifetime(ContainerLifetime.Persistent);
        });

    var vehiclesTopic = resourceBuilder.AddServiceBusTopic("vehicles");
    vehiclesTopic.AddServiceBusSubscription("apiserviceconsumer", "apiservice");
    return resourceBuilder;
}

I asked Claude Code: “Add a second subscriber to the vehicles topic — a separate worker service that processes vehicle events for reporting.”

The result was concrete and usable. Claude Code picked up the existing AddServiceBusSubscription pattern, added a new subscription to the extension method, and wrote the corresponding addition to the AppHost:

vehiclesTopic.AddServiceBusSubscription("reportingconsumer", "reporting");

var reportingWorker = builder
    .AddProject<Projects.Grouply_ReportingWorker>("reporting")
    .WithReference(serviceBus)
    .WaitFor(serviceBus)
    .WithReference(postgresdb)
    .WaitFor(postgresdb);

Where Claude Code stops

Claude Code is good at reading patterns and extending what’s already there. But it doesn’t automatically wire up every Aspire concern.

A consistent example: when adding a new project or worker service, Claude Code typically forgets to call builder.AddServiceDefaults() in the new service’s Program.cs. That single call sets up health checks, OpenTelemetry tracing, service discovery, HttpClient defaults, and resilience — everything that makes a standalone service a proper Aspire participant.

You have to ask. Explicitly: “Did you add AddServiceDefaults() to the new worker?” Then it adds the line and explains why it matters.

That pattern holds broadly. Claude Code helps you extend the dependency graph and reason about startup order. Infrastructure bootstrapping — the calls that wire a service into the Aspire runtime — remains your responsibility to verify.


Debugging startup failures

Aspire has an excellent dashboard with logs and health checks. But when something doesn’t start, you need to know where to look.

A concrete example from Grouply: the API started up, but JWT authentication failed in Azure because the issuer URL was wrong. Locally, the API used the internal Keycloak address for both the metadata fetch and token validation. In Azure Container Apps, it needed the internal HTTP address for the metadata fetch, but the external HTTPS address for issuer validation in the tokens themselves.

The configuration chain runs through three layers: the WithEnvironment() call in the AppHost, builder.Configuration in the API’s Program.cs, and finally TokenValidationParameters. That’s a lot to trace manually.

I gave Claude Code the error message (IDX10205: Issuer validation failed), the relevant section from the AppHost, the auth configuration from Program.cs, and the relevant environment variables. The quality of the diagnosis depends heavily on that context — give it only the error message and you get a generic answer. Give it the full configuration chain and it reconstructs the path and identifies the root cause: ValidIssuer in TokenValidationParameters used the internal HTTP address from the Aspire connection string, but the token contained the external HTTPS address.

The fix was a separate configuration value:

// AppHost: pass external HTTPS address as a separate env var
if (builder.ExecutionContext.IsPublishMode && keycloakHostname != null)
{
    var keycloakIssuerUrl = ReferenceExpression.Create($"https://{keycloakHostname}");
    apiService.WithEnvironment("Keycloak__IssuerUrl", keycloakIssuerUrl);
}

// API: use internal address for metadata, external for validation
options.Authority = $"{keycloakMetadataUrl}/realms/{realm}";
ValidIssuer = $"{keycloakIssuerUrl}/realms/{realm}"

Without Claude Code, I would have traced this manually through three files. With Claude Code, it was a matter of providing the right context and letting it reconstruct the chain.


Local vs. Azure: IsPublishMode

Aspire’s strongest feature is also the source of the most confusion: the same AppHost runs locally with Docker containers and in Azure with managed services. builder.ExecutionContext.IsPublishMode is the switch.

In Grouply, that looks like this:

if (builder.ExecutionContext.IsPublishMode)
{
    keycloakAdminPassword = builder.AddParameter("keycloak-password", secret: true).Resource;
    keycloakHostname = builder.AddParameter("keycloak-hostname").Resource;
}

Locally, those parameters are ignored and the containers run with default settings. In Azure, they’re filled in from Bicep parameters.

The trap: you build locally, everything works, you deploy to Azure, and something breaks because you forgot an environment-specific setting. Claude Code helps concretely here — point it at your AppHost and ask for a targeted review. Good questions are:

  • “Compare the local and IsPublishMode paths. Which services are configured differently and why?”
  • “Are there values that are hardcoded locally but should come from a parameter or secret in production?”
  • “Which services have WithReference relationships but no matching WaitFor?”

These are checks you’d normally do manually at the moment something breaks. Claude Code does them proactively, before the deployment.


What changes

Aspire and Claude Code are a good pair for one reason: Aspire makes your application architecture inspectable. The topology is explicit in code. Not everything disappears from appsettings or environment variables, but the relationships that matter — who depends on whom, what must start before what — are readable in one file.

Claude Code becomes useful when the architecture is inspectable. Aspire delivers that.

Already using Aspire? Here are the exact checks I now run on every AppHost:

Review this .NET Aspire AppHost. Check:
- missing WithReference / WaitFor pairs
- services without AddServiceDefaults()
- local vs publish-mode differences
- hardcoded URLs or ports
- missing parameters/secrets for Azure
- inconsistent service names
- health check or startup-order risks

Not using Aspire yet? Start small: one AddProject(), one AddRedis(), and ask Claude Code to wire things up. The orchestration layer is the hardest part to hold in your head — that’s exactly what you can hand off.