Aspire has moved fast enough — from a .NET-only preview to a polyglot, rebranded platform with its own CLI in a few years — that interviewers now use it to separate candidates who have actually run it against a real multi-service system from candidates who have only read the getting-started page. The questions below probe whether you understand what Aspire actually is (an opinionated development and deployment toolchain) versus what it is often mistaken for (a runtime, a service mesh, a hosting platform), because that confusion is exactly where weaker answers fall apart. For engineers with a decade or more of distributed-systems experience, the interesting ground is the AppHost's resource model, how service defaults turn observability and resilience into structural defaults instead of per-service boilerplate, and how Aspire's deployment story and its relationship to Docker Compose, Dapr and the now-archived Project Tye actually fit together. The ten questions below cover exactly that.
Q1 What problem does .NET Aspire actually solve, and what is it explicitly not?#
Short answer: Aspire solves the "how do I run and understand my whole distributed system locally, consistently, without a pile of scripts and copied connection strings" problem, through a C# (or, since Aspire went polyglot, TypeScript) AppHost that declares every service, database and cloud resource as a graph, runs that graph with one command, and gives every service consistent telemetry and resilience defaults. It is explicitly not a runtime framework, a service mesh, or a hosting platform — your services reference ordinary libraries, run fine without the AppHost present, and no production request ever passes through it.
Before Aspire, a typical multi-service .NET solution accumulated a docker-compose.yaml a new developer had to learn separately, a README of manual steps ("start the API, then the worker, then run migrations, then start the frontend"), and connection strings copied and often silently drifting between services' appsettings.json files. Aspire's AppHost replaces that pile with one strongly-typed C# program that is itself part of the solution, so "how do I run this system" has one authoritative, versioned answer instead of a README that goes stale. The scope is intentionally bounded, though, and the project's own FAQ is explicit about it: Aspire is a development-time orchestrator and a deployment pipeline generator, not a piece of infrastructure your production system depends on at request time. That distinction is also why Aspire coexists with, rather than replaces, a real service mesh or a runtime like Dapr — those operate inside the running system, while Aspire operates around it, at build, run-locally and deploy time.
What interviewers look for: a precise boundary between "development and deployment tool" and "runtime dependency," since that boundary is the single most common source of confused answers about what Aspire actually does.
Common mistakes:
- Describing Aspire as a service mesh or claiming production traffic flows through it.
- Only mentioning the dashboard, missing the bigger point that the AppHost is a strongly-typed, versioned replacement for ad hoc scripts and READMEs.
Q2 Walk through the AppHost's resource model — what is a resource, and how do WithReference and WaitFor work together?#
Short answer: A resource is any node in the application graph the AppHost manages — a .NET project, a container, an executable, a cloud service, a connection string or a parameter — and it carries annotations describing its endpoints, environment variables and relationships to other resources. WithReference injects one resource's connection information into a consumer (a connection string, or service-discovery configuration for a project-to-project reference), while WaitFor delays starting a resource until its dependency is actually running and healthy, so the two together are what let you declare "this API needs this database, and shouldn't start serving until it's ready" instead of writing a retry loop by hand.
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres").WithDataVolume();
var catalogDb = postgres.AddDatabase("catalogdb");
var migrations = builder.AddProject<Projects.Shop_MigrationService>("migrations")
.WithReference(catalogDb)
.WaitFor(catalogDb);
var api = builder.AddProject<Projects.Shop_CatalogApi>("catalog-api")
.WithReference(catalogDb)
.WaitForCompletion(migrations)
.WithHttpHealthCheck("/health");
builder.Build().Run();WaitForCompletion is the variant worth calling out specifically: it waits for a resource to exit successfully rather than just become healthy, which is exactly the shape a one-shot database migration job needs — the API should wait for migrations to finish, not merely start. Underneath WithReference, service discovery does the real work at run time: when project A references project B, the AppHost injects endpoint configuration, and a discovery library resolves a logical URI such as https+http://catalog-api to whatever port was actually allocated for that run, preferring HTTPS and falling back to HTTP. That logical-name indirection is what keeps hard-coded ports and hostnames out of service code entirely, and it's scoped deliberately — discovery configuration exists only for resources you reference explicitly, so coupling between services stays visible in the AppHost rather than implicit.
What interviewers look for: the distinction between WaitFor (healthy) and WaitForCompletion (exited successfully), and a correct mental model of service discovery as configuration injection plus logical-name resolution, not literal port forwarding.
Follow-up questions:
- What happens if you reference a resource with
WithReferencebut never add a matchingWaitFor? - How would you model a resource that should only start in one of the AppHost's two modes?
Q3 What does the ServiceDefaults project actually give you, and why does it matter that it's your own code rather than a NuGet black box?#
Short answer: ServiceDefaults is a shared project every service in an Aspire solution references, exposing AddServiceDefaults and MapDefaultEndpoints, and in the current template it wires up OpenTelemetry logging, metrics and tracing, health checks, service discovery, and standard HTTP resilience (retry, circuit breaker, timeout) for every outgoing HttpClient — all in one place. It matters that it is plain source code you own, not a compiled package, because a platform team can extend or change those defaults for every service at once by editing one project, and nothing about the approach is magic or hidden from a code reviewer.
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb");
var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();The deliberate default worth knowing precisely: MapDefaultEndpoints maps /health (every registered check must pass) and /alive (only checks tagged live must pass), but only in the Development environment, because an open health endpoint can leak information or become a denial-of-service target in production. Teams that expect /health to exist after deploying are hitting exactly this default, and the fix is to map those endpoints deliberately for production — typically on an internal management port that isn't publicly routed — rather than assuming the template's development convenience carries over unchanged. Because ServiceDefaults is your own project, the natural extensions are things like EF Core or gRPC instrumentation, an Azure Monitor exporter the template leaves commented out by default, or restricting service discovery to HTTPS only — changes a platform team makes once, in one file, that every service picks up on its next build.
What interviewers look for: the specific "only in Development" health-endpoint default and the architectural point that ServiceDefaults being owned source code, not a package, is what makes org-wide defaults maintainable at all.
Common mistakes:
- Assuming
/healthis automatically available after a production deployment. - Treating ServiceDefaults as fixed framework code instead of a project meant to be extended.
Q4 How do hosting integrations and client integrations pair up in Aspire, and how would you approach adding one for a new Azure resource?#
Short answer: A hosting integration extends the AppHost with a resource type — AddPostgres from Aspire.Hosting.PostgreSQL, for example — while a matching client integration registers the corresponding SDK inside a consuming service, such as AddNpgsqlDbContext from Aspire.Npgsql.EntityFrameworkCore.PostgreSQL, with health checks, logging, tracing and metrics enabled by default. The two packages share only a connection name; the client integration works even without an AppHost, as long as a connection string under that name exists in ordinary configuration, which is what keeps services from becoming hard-dependent on Aspire at run time.
Azure hosting integrations follow a consistent, useful pattern for local development: RunAsEmulator or RunAsContainer swaps in a local emulator or an open-source container in place of the real Azure service while running locally, and the AppHost generates the actual Azure infrastructure (typically as Bicep) only when you publish.
var serviceBus = builder.AddAzureServiceBus("messaging").RunAsEmulator();
serviceBus.AddServiceBusQueue("orders");
var ordersDb = builder.AddAzurePostgresFlexibleServer("pg")
.RunAsContainer()
.AddDatabase("ordersdb");
builder.AddProject<Projects.Orders_Worker>("orders-worker")
.WithReference(serviceBus)
.WithReference(ordersDb)
.WaitFor(serviceBus);Adding a brand-new integration for a resource Aspire doesn't yet cover means writing both halves yourself: a hosting-side IResourceBuilder<T> extension that adds the resource, its endpoints and any RunAsEmulator/RunAsContainer equivalent for local development, and a client-side extension that resolves the connection name from configuration and registers the SDK with the same health-check and telemetry conventions ServiceDefaults already establishes — matching that convention is what makes a homegrown integration feel native rather than bolted on. aspire add is the day-to-day command for pulling in an existing integration with matching AppHost and client package versions rather than resolving them by hand.
What interviewers look for: correctly describing the hosting/client pairing and the shared connection-name contract, plus the insight that client integrations degrade gracefully without an AppHost, which is what keeps Aspire's coupling loose rather than tight.
Common mistakes:
- Believing a client integration hard-depends on the AppHost being present at run time.
- Forgetting that a typo in a resource name silently produces a configuration error at service startup, not a compile error, since the connection name is a string contract between the two sides.
Q5 How do you test an Aspire-orchestrated application, and how is that different from testing a single service with WebApplicationFactory?#
Short answer: Aspire.Hosting.Testing provides DistributedApplicationTestingBuilder, which starts your real AppHost — including its containers — inside a test process, making it the closest thing to a true end-to-end test you can run on a laptop or CI agent; WebApplicationFactory, by contrast, tests one service in isolation, in-process, with no other real services or containers involved. The two are complementary, not competing: use WebApplicationFactory for fast, focused tests of one service's HTTP behavior, and DistributedApplicationTestingBuilder for the smaller set of scenarios where you actually need to prove the whole wired-together system behaves correctly.
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.Shop_AppHost>(ct);
appHost.Services.ConfigureHttpClientDefaults(http => http.AddStandardResilienceHandler());
await using var app = await appHost.BuildAsync(ct).WaitAsync(TimeSpan.FromSeconds(90), ct);
await app.StartAsync(ct);
await app.ResourceNotifications
.WaitForResourceHealthyAsync("webfrontend", ct)
.WaitAsync(TimeSpan.FromSeconds(90), ct);
using var client = app.CreateHttpClient("webfrontend");
using var response = await client.GetAsync("/", ct);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);CreateHttpClient resolves the real allocated endpoint for a named resource, and GetConnectionStringAsync on the running application hands you a real connection string so a test can seed or assert against actual data rather than a stand-in. The practical cost is startup time — real containers have to come up — so these tests are necessarily slower than an in-process WebApplicationFactory suite, which argues for keeping the scenario count focused (critical cross-service journeys, not every edge case) and sharing one running application per test class through a fixture rather than starting the whole graph per test method. CI agents also need an actual container runtime available, which is worth confirming explicitly rather than discovering when the pipeline fails.
What interviewers look for: a clear articulation of when each testing approach earns its cost, and concrete API knowledge (WaitForResourceHealthyAsync, CreateHttpClient, GetConnectionStringAsync) rather than a vague "Aspire has testing support."
Common mistakes:
- Replacing all
WebApplicationFactorytests with AppHost-level tests, making the suite far slower than it needs to be for what most tests are actually verifying. - Starting a fresh AppHost per test method instead of sharing one per test class, multiplying container startup cost across the whole suite.
Q6 How does deployment work in Aspire — what's the difference between aspire publish and aspire deploy, and which targets are supported?#
Short answer: You declare one or more compute environments in the AppHost, and their pipeline steps translate the same resource graph into real deployment artifacts; aspire publish generates those artifacts (Bicep, Compose files or Helm charts) and deliberately leaves parameters unresolved so other tooling can apply them, while aspire deploy resolves parameters, builds and pushes images, and applies the changes in a single step. Supported compute environments span Azure Container Apps and Azure App Service (both stable), Docker Compose (stable), and Kubernetes and AKS (still preview packages), so the same AppHost model can target very different platforms depending on which environment a service is bound to.
var aca = builder.AddAzureContainerAppEnvironment("aca");
builder.AddProject<Projects.Shop_CatalogApi>("catalog-api")
.WithComputeEnvironment(aca)
.PublishAsAzureContainerApp((infra, app) =>
{
app.Template.Scale.MinReplicas = 1;
app.Template.Scale.MaxReplicas = 10;
});aspire publish --output-path ./artifacts # Bicep, compose files or Helm charts
aspire deploy --environment Production # resolve parameters, push images, apply
aspire destroy # tear the deployment down againThe Azure Developer CLI (azd up) still works end to end and remains a reasonable choice for teams with an existing azd pipeline, but the project's own guidance no longer treats it as the default path forward, because it consumes an older deployment manifest format, and new Azure deployment capabilities land in aspire deploy first — worth knowing precisely, since a candidate who only knows azd is describing what used to be the primary path rather than the current one. Kubernetes support deserves a specific caveat too: AddKubernetesEnvironment and AddAzureKubernetesEnvironment are still preview packages, so the generated Helm chart should be reviewed like any other generated infrastructure before it's trusted in production, the same discipline you'd apply to any Bicep or Terraform output you didn't hand-write.
What interviewers look for: the precise publish-versus-deploy distinction (artifact generation with unresolved parameters versus a full resolve-build-push-apply step), and current, accurate knowledge of which targets are stable versus preview rather than treating them all as equally production-ready.
Common mistakes:
- Conflating
aspire publishandaspire deployas the same command with different names. - Treating Kubernetes support as equally mature as the Azure Container Apps and App Service paths.
Q7 How does Aspire compare to a docker-compose.yaml file for local development?#
Short answer: Both let you start a whole system with one command, but Compose describes infrastructure declaratively in YAML with hand-written environment variables and depends_on health conditions for startup ordering, while Aspire's AppHost is executable C# with WithReference injecting connection information automatically and WaitFor backed by real health checks — and, unlike Compose, Aspire runs your .NET projects as local processes you can debug directly from the IDE rather than only as built containers.
The practical difference shows up most at the edges Compose doesn't really have: Aspire's built-in dashboard gives every resource's logs, traces and metrics over OTLP with no extra stack to stand up, DistributedApplicationTestingBuilder turns the same graph into a real integration test, and aspire publish/aspire deploy turn it into deployable artifacts for several different targets — a Compose file, by contrast, is usually a separate artifact from whatever actually deploys to production, maintained by hand alongside it. That said, Compose is not obsolete inside Aspire itself: AddDockerComposeEnvironment is one of Aspire's own supported deployment targets, generating a real docker-compose.yaml and .env file from the same AppHost model — so a team can author the system once in C# and still hand a Compose file to a deployment process that expects one, rather than choosing between the two approaches permanently. Compose remains a reasonable, simpler choice for a small system with few services and no .NET debugging need, or a genuinely polyglot team with no interest in a C# AppHost at all.
What interviewers look for: naming Aspire's concrete advantages precisely (debuggable local processes, automatic connection injection, health-backed startup ordering, built-in telemetry) rather than a vague "Aspire is better," and the detail that Aspire can itself emit Compose files, which most candidates miss.
Common mistakes:
- Describing Aspire as strictly incompatible with Docker Compose instead of noting it can generate Compose output as one of its own deployment targets.
- Ignoring the debugging difference — running .NET projects as native local processes versus only ever as containers.
Q8 How does Aspire relate to Dapr — do they compete, or do they solve different problems?#
Short answer: They operate at different layers and are largely complementary rather than competing: Dapr is a sidecar-based runtime that ships alongside your application in production, providing building blocks such as state management, pub/sub messaging, service invocation and actors behind a consistent API, while Aspire is a development-time orchestrator and deployment pipeline generator with no presence in the production request path at all. A system can legitimately use both — Aspire orchestrating local development and deployment, with individual services using Dapr's sidecar for the distributed-systems building blocks Aspire deliberately doesn't try to reimplement.
Dapr's .NET SDK (Dapr.Client, Dapr.AspNetCore, Dapr.Actors, Dapr.Workflow, among other packages) lets a service call state management, pub/sub and actor APIs through a local sidecar process, which is itself a genuine production runtime dependency — unlike anything in Aspire. The concrete bridge between the two projects is the Aspire Community Toolkit's CommunityToolkit.Aspire.Hosting.Dapr package, which adds Dapr-specific resources to the AppHost so you can declare a Dapr state store or pub/sub component and wire a project to its sidecar the same way you'd wire it to a database:
var stateStore = builder.AddDaprStateStore("statestore");
var pubSub = builder.AddDaprPubSub("pubsub");
builder.AddProject<Projects.Orders_Api>("orders-api")
.WithDaprSidecar()
.WithReference(stateStore)
.WithReference(pubSub);The honest framing for an interview is that they answer different questions: "how do I run, observe and deploy my whole system" (Aspire) versus "how do I get consistent, portable building blocks for state, messaging and service calls inside my running services" (Dapr). A team already committed to Dapr's sidecar model for its building blocks gains an easier local development and deployment story by orchestrating it with Aspire, rather than having to choose one project over the other.
What interviewers look for: the layering insight — production runtime dependency (Dapr) versus development/deployment tooling (Aspire) — and awareness that a real integration path exists between them rather than treating the two as mutually exclusive alternatives.
Common mistakes:
- Framing Aspire and Dapr as direct competitors solving the same problem.
- Not knowing that Aspire has no production runtime footprint at all, which is precisely why comparing it feature-for-feature against a sidecar runtime like Dapr is the wrong comparison to begin with.
Q9 What was Project Tye, and why is .NET Aspire widely considered its spiritual successor?#
Short answer: Project Tye was an earlier Microsoft experiment — an open-source local orchestrator for making .NET microservices easier to develop, letting you run several services with one command, use containerized dependencies, and deploy to Kubernetes with minimal configuration. Its GitHub repository was archived by its owner in November 2023 and is now read-only, around the same period Aspire began emerging as Microsoft's actively developed answer to largely the same local-orchestration problem, with a much broader integration ecosystem, a real dashboard, and first-class testing and deployment tooling that Tye never reached as a production-grade offering.
The two projects share an obvious ambition — stop making developers hand-roll scripts to run a multi-service .NET system locally — but Aspire arrived with resources that go well beyond what Tye's tye.yaml-based model offered: a strongly-typed C# resource graph instead of YAML, paired hosting and client integrations with health checks and telemetry built in by convention, a dashboard with real distributed tracing over OTLP rather than basic log aggregation, and a deployment pipeline with multiple supported targets instead of a single Kubernetes-focused path. For an interview answer, the useful framing is historical accuracy rather than a marketing line: Tye was archived, Aspire is the actively maintained project addressing the same core need today, and a candidate who can name Tye at all, correctly, is usually demonstrating real tenure in the .NET ecosystem rather than reciting a comparison they read for the interview.
What interviewers look for: accurate historical context (Tye is archived, not merely "older"), without overclaiming a formal, official "successor" relationship the projects' own documentation doesn't explicitly state — a nuanced, honest answer here reads as more credible than a confident but unsupported claim.
Common mistakes:
- Not knowing Tye existed at all, which is a reasonable gap for less tenured candidates but worth noting as a follow-up learning point.
- Overstating the relationship as an official, documented rename or migration path rather than two separate projects solving a similar problem years apart.
Q10 You're asked to bring an existing six-service .NET solution, currently run with a docker-compose file and a README full of manual steps, under Aspire. Walk through your migration approach.#
Short answer: Migrate incrementally rather than rewriting everything at once: add an AppHost alongside the existing Compose setup, bring services in one at a time starting with the one that has the fewest dependencies, replace hard-coded connection strings and ports with WithReference and service discovery as each service moves over, and keep the Compose file working throughout the migration as a fallback until the AppHost fully replaces it.
Concretely: first, scaffold the AppHost and ServiceDefaults projects and add the first, simplest service (commonly a leaf service with no downstream dependencies) as an AddProject resource, proving the basic loop — aspire run, dashboard, logs — works before touching anything else. Second, add its infrastructure dependencies (a database, a cache) as Aspire resources using RunAsContainer or the matching hosting integration, replacing the connection strings the old Compose file hard-coded with WithReference, and delete that piece from the Compose file only once the AppHost path is verified working end to end — never both at once pointing at the same infrastructure, which risks two paths fighting over the same container. Third, work through the remaining services in dependency order, adding WaitFor (or WaitForCompletion for one-shot jobs like migrations) as each service's real startup dependencies become explicit — this step alone often surfaces implicit ordering assumptions the README's manual steps were silently encoding, such as "always start the auth service before anything else," which had never been written down as an actual dependency. Fourth, once every service runs under the AppHost, retrofit ServiceDefaults into each one for consistent telemetry and resilience, add a DistributedApplicationTestingBuilder smoke test covering the critical cross-service path, and only then retire the Compose file and the README's manual steps — or, if the team still needs a Compose artifact for an existing deployment process, generate one from the AppHost with AddDockerComposeEnvironment instead of hand-maintaining two divergent definitions of the same system.
What interviewers look for: an incremental, dependency-ordered migration plan with an explicit fallback during the transition, plus the insight that the migration itself tends to surface undocumented implicit dependencies the manual README was quietly compensating for.
Common mistakes:
- Attempting a big-bang rewrite of all six services into the AppHost at once instead of an incremental, verifiable migration.
- Running the same infrastructure dependency from both the old Compose file and the new AppHost simultaneously, causing port or state conflicts during the transition.
Quick-Fire Round#
| Question | Answer |
|---|---|
| Does production traffic ever pass through the AppHost? | No — Aspire has no runtime footprint in production. |
| Which method waits for a resource to exit successfully rather than just become healthy? | WaitForCompletion. |
In which environment does MapDefaultEndpoints map health endpoints by default? | Development only. |
| What starts a real AppHost, including containers, inside a test process? | DistributedApplicationTestingBuilder. |
| Which command generates deployment artifacts but leaves parameters unresolved? | aspire publish. |
| Which command resolves parameters, pushes images and applies changes in one step? | aspire deploy. |
| Can Aspire generate a docker-compose.yaml as a deployment target? | Yes, via AddDockerComposeEnvironment. |
| When was the Project Tye GitHub repository archived? | November 2023. |
How to Prepare#
- Build a small AppHost with at least three resources — a database, an API and a worker — using
WaitForandWaitForCompletioncorrectly, so you can describe the resource model from direct experience. - Deliberately break a connection name between a hosting and client integration and observe the failure, so you can explain that contract precisely.
- Write one
DistributedApplicationTestingBuildertest and oneWebApplicationFactorytest for the same service, and be ready to explain why you'd reach for each. - Run
aspire publishagainst a sample AppHost and read the generated Bicep or Compose output, so deployment isn't an abstract description. - Be ready to place Aspire, Dapr and Kubernetes on the same diagram and explain what each one actually owns at run time versus at development and deployment time.