Je opent een Aspire-project. De AppHost ziet er anders uit dan alle andere .NET code die je kent. Geen controllers, geen services, geen middleware. Alleen AddRedis(), AddProject<>(), WithReference() en WaitFor(). Bedrading, geen logica.

De eerste keer is dat verwarrend. Waar komt de configuratie vandaan? Waarom wacht deze service op die? Wat gebeurt er als iets niet opstart?

Claude Code is hier een verrassend goede gids — en dat heeft een specifieke reden.


De AppHost is je architectuurkaart

In de meeste .NET-applicaties staat de architectuur verspreid over tientallen bestanden. Services worden geregistreerd in Program.cs, configuratie staat in appsettings.json, en de samenhang tussen componenten moet je reconstrueren uit het geheel.

Aspire doet iets anders. De AppHost maakt de topologie van je applicatie expliciet in één bestand. Veel meer van de applicatiestructuur wordt zichtbaar in code. Niet alles verdwijnt uit configuratie, maar de belangrijke relaties — welke service afhankelijk is van welke, wie wacht op wie — zijn makkelijker te inspecteren. Dat is precies het soort code waar Claude Code goed mee overweg kan.

Neem de AppHost van Grouply, een multi-tenant fleet management applicatie:

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

Het verschil tussen die twee methodes is belangrijk:

  • WithReference() maakt de connectie beschikbaar: het injecteert de connectionstring of het serviceadres in de omgeving van de afhankelijke service, zodat service discovery werkt.
  • WaitFor() regelt de opstartvolgorde: de service start pas als de dependency een healthy state bereikt.

Wijs Claude Code naar de AppHost en het gerelateerde service-project, en vraag: “Leg uit wat hier gebeurt en wat er misgaat als je een WaitFor weglaat.” Je krijgt een uitleg van de opstartvolgorde en de soorten fouten die je kunt verwachten — startup races, transient connection failures, auth-fouten omdat de identity provider nog niet klaar is. De exacte foutmelding hangt af van de dependency en het retry-gedrag van je services, maar de klasse van het probleem is duidelijk.

Dat is geen magie. Claude Code leest de afhankelijkheidsgraph gewoon af uit de code — omdat Aspire die graph expliciet heeft gemaakt.


Een nieuwe service toevoegen

De meest voorkomende taak in een Aspire-project: een nieuwe service of resource toevoegen en correct bedraden.

In Grouply had ik een worker nodig die luistert op het vehicles topic van de Azure Service Bus. De Service Bus was al geconfigureerd in de AppHost via een extensiemethode:

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;
}

Ik vroeg Claude Code: “Voeg een tweede subscriber toe op het vehicles topic — een aparte worker service die voertuiggebeurtenissen verwerkt voor rapportage.”

Het resultaat was concreet en bruikbaar. Claude Code pakte het bestaande patroon van AddServiceBusSubscription op, voegde een nieuwe subscription toe in de extensiemethode, en schreef de bijbehorende toevoeging in de AppHost:

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

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

Waar Claude Code stopt

Claude Code is goed in het lezen van patronen en het uitbreiden van wat er al is. Maar niet elke Aspire-concern wordt automatisch meegenomen.

Een terugkerend voorbeeld: bij het toevoegen van een nieuw project of worker-service vergeet Claude Code typisch om builder.AddServiceDefaults() aan te roepen in de Program.cs van de nieuwe service. Die ene aanroep configureert health checks, OpenTelemetry-tracing, service discovery, HttpClient defaults en resilience — alles wat van een losse service een volwaardige Aspire-deelnemer maakt.

Je moet er zelf naar vragen. Expliciet: “Heb je AddServiceDefaults() toegevoegd aan de nieuwe worker?” Dan voegt het de regel toe en legt het uit waarom die nodig is.

Dat patroon geldt breder. Claude Code helpt je de dependency-graph uitbreiden en over opstartvolgorde nadenken. Infrastructure bootstrapping — de aanroepen die een service in de Aspire-runtime koppelen — blijft jouw verantwoordelijkheid om te controleren.


Opstartproblemen debuggen

Aspire heeft een uitstekend dashboard met logs en health checks. Maar als iets niet start, moet je weten waar je moet kijken.

Een concreet voorbeeld uit Grouply: de API startte op, maar JWT-authenticatie faalde in Azure omdat de issuer URL niet klopte. Lokaal gebruikte de API het interne Keycloak-adres voor zowel metadata-fetch als token-validatie. In Azure Container Apps moest het interne HTTP-adres voor de metadata-fetch, maar het externe HTTPS-adres voor de issuer-validatie in de tokens zelf.

De configuratieketen loopt door drie lagen: de WithEnvironment() aanroep in de AppHost, de builder.Configuration in Program.cs van de API, en uiteindelijk de TokenValidationParameters. Dat is veel om handmatig te traceren.

Ik gaf Claude Code de foutmelding (IDX10205: Issuer validation failed), het relevante stuk uit de AppHost, de auth-configuratie uit Program.cs en de relevante omgevingsvariabelen. De kwaliteit van de diagnose hangt sterk af van die context — geef je alleen de foutmelding, dan krijg je een generiek antwoord. Geef je de volledige configuratieketen, dan reconstrueert het de keten en wijst het de oorzaak aan: de ValidIssuer in TokenValidationParameters gebruikte het interne HTTP-adres uit de Aspire-connectionstring, maar de token bevatte het externe HTTPS-adres.

De oplossing was een aparte configuratiewaarde:

// AppHost: geef het externe HTTPS-adres mee als aparte env var
if (builder.ExecutionContext.IsPublishMode && keycloakHostname != null)
{
    var keycloakIssuerUrl = ReferenceExpression.Create($"https://{keycloakHostname}");
    apiService.WithEnvironment("Keycloak__IssuerUrl", keycloakIssuerUrl);
}

// API: gebruik het interne adres voor metadata, externe voor validatie
options.Authority = $"{keycloakMetadataUrl}/realms/{realm}";
ValidIssuer = $"{keycloakIssuerUrl}/realms/{realm}"

Zonder Claude Code had ik dit handmatig moeten traceren door drie bestanden. Met Claude Code was het een kwestie van de juiste context aanleveren en de keten laten reconstrueren.


Lokaal vs. Azure: IsPublishMode

Aspire’s sterkste eigenschap is ook de bron van de meeste verwarring: dezelfde AppHost draait lokaal met Docker-containers en in Azure met managed services. builder.ExecutionContext.IsPublishMode is de schakelaar.

In Grouply ziet dat er zo uit:

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

Lokaal worden die parameters genegeerd en draaien de containers met standaardinstellingen. In Azure worden ze ingevuld vanuit de Bicep-parameters.

De valkuil: je bouwt lokaal, alles werkt, je deployt naar Azure en iets faalt omdat je een environment-specifieke instelling vergeten bent. Claude Code helpt hier concreet — wijs het naar je AppHost en vraag een gerichte review. Goede vragen zijn:

  • “Vergelijk de lokale en IsPublishMode-paden. Welke services worden anders geconfigureerd en waarom?”
  • “Zijn er waarden die lokaal hardcoded zijn maar in productie via een parameter of secret moeten komen?”
  • “Welke services hebben WithReference-relaties maar geen bijbehorende WaitFor?”

Dat zijn checks die je normaal handmatig doet op het moment dat het misgaat. Claude Code doet ze proactief, voor de deployment.


Wat dit verandert

Aspire en Claude Code zijn een goed koppel om één reden: Aspire maakt de architectuur van je applicatie inspecteerbaar. De topologie staat expliciet in code. Niet alles verdwijnt uit appsettings of omgevingsvariabelen, maar de relaties die ertoe doen — wie afhankelijk is van wie, wat moet opstarten voor wat — zijn leesbaar in één bestand.

Claude Code wordt nuttig als de architectuur inspecteerbaar is. Aspire levert dat.

Gebruik je Aspire al? Hier zijn de exacte checks die ik het nu laat uitvoeren op elke 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

Gebruik je Aspire nog niet? Begin klein: één AddProject(), één AddRedis(), en vraag Claude Code om de wires te leggen. De orchestratielaag is het moeilijkste om in je hoofd te houden — dat is precies wat je kunt uitbesteden.