Nitin Kumar SinghSolutions Architect

Type to search. to move, Enter to open.

    move open esc close
    Deep Dive.NET

    Adding .NET Aspire to an Existing Clean Architecture Project

    Add .NET Aspire to an existing Clean Architecture app — AppHost orchestration, service discovery, and OpenTelemetry — without a rewrite.

    On more than one project I’ve watched the first sprint disappear into Docker Compose files and appsettings drift: standing up databases, wiring service discovery, getting distributed tracing working, keeping connection strings in sync across environments. None of it is business logic, and all of it has to work before the business logic can.

    I already had a Clean Architecture app running (Angular frontend, .NET API, PostgreSQL) with its own Docker Compose setup. Rather than rewrite it, I put .NET Aspire on top and let the AppHost own orchestration. The whole migration came down to two new projects, one line in Program.cs, and a project reference. This post is what I changed, in order, and what each change bought me.

    Adding Aspire to an Existing Project

    You don’t need to start from scratch. This project shows how to add Aspire to an existing Clean Architecture application. Here’s what I added:

    Project Structure Changes

    aspire/ # NEW - Aspire orchestration
    ├── AppHost/
    │ ├── AppHost.cs # Service definitions
    │ └── Contact.AppHost.csproj # AppHost project file
    └── ServiceDefaults/
    ├── Extensions.cs # OpenTelemetry, health checks
    └── Contact.ServiceDefaults.csproj

    Steps to Add Aspire to Your Existing Project

    1. Install the Aspire workload:

      Terminal window
      dotnet workload install aspire
    2. Create the AppHost project:

      Terminal window
      dotnet new aspire-apphost -n YourApp.AppHost
    3. Create the ServiceDefaults project:

      Terminal window
      dotnet new aspire-servicedefaults -n YourApp.ServiceDefaults
    4. Reference ServiceDefaults in your API project:

      <ProjectReference Include="..\ServiceDefaults\YourApp.ServiceDefaults.csproj" />
    5. Add ServiceDefaults to your Program.cs:

      var builder = WebApplication.CreateBuilder(args);
      builder.AddServiceDefaults(); // Add this line
      // ... your existing code ...
      var app = builder.Build();
      app.MapDefaultEndpoints(); // Add health check endpoints
      app.Run();
    6. Configure your AppHost to orchestrate services (see Deep Dive section below)

    For detailed instructions, see Microsoft’s official guide: Add .NET Aspire to an existing .NET app


    The Problem with Traditional Microservices Development

    The same friction shows up on every project. Here’s where the time goes:

    Common Challenges Include:

    ChallengeImpact
    Service DiscoveryManually managing URLs and ports across environments
    Connection StringsDuplicating database configs in multiple places
    ObservabilitySetting up separate tools for logs, traces, and metrics
    Container ManagementWriting complex docker-compose files
    Health MonitoringImplementing custom health check endpoints
    Environment VariablesManaging different configs for dev/staging/prod

    What is .NET Aspire?

    .NET Aspire is a stack for building observable, distributed applications. At its core is the AppHost: a code-first orchestrator that defines your application’s services, resources, and connections.

    Key Capabilities

    1. AppHost Orchestration: Define services, dependencies, and configuration in code - no YAML required
    2. Rich Integrations: NuGet packages for popular services with standardized interfaces
    3. Built-in Observability: OpenTelemetry integration out of the box
    4. Developer Dashboard: Real-time visibility into all your services
    5. Consistent Tooling: Works with Visual Studio, VS Code, and CLI

    Architecture Overview: This Project with Aspire

    Here’s how this Clean Architecture project is orchestrated with .NET Aspire:

    BrowserOrchestrated servicesAspire dashboardStarted and wired by AppHostWhat it showsUserAngular frontendnpm serve.NET APIclean architecture — contact-apiPostgreSQLcontactsdbpgAdmindatabase managementLogsTracesMetricsResourcesHTTPtelemetry
    Aspire does not sit in the request path. Traffic runs left to right exactly as it did before; what changes is that every service reports telemetry to a dashboard, and the connection strings between them stop being written by hand.

    Service Flow Explained

    sequenceDiagram
    participant U as User
    participant F as Angular Frontend
    participant A as .NET API
    participant D as PostgreSQL
    participant T as Aspire Dashboard
    Note over U,T: All services automatically discovered via Aspire
    U->>F: 1. Navigate to app
    F->>A: 2. API Request (service://contact-api)
    Note over F,A: Service Discovery - No hardcoded URLs!
    A->>D: 3. Query Database
    Note over A,D: Connection string injected by Aspire
    D-->>A: 4. Return Data
    A-->>F: 5. JSON Response
    F-->>U: 6. Render UI
    Note over A,T: OpenTelemetry sends traces/metrics
    A--)T: Trace Data
    F--)T: Trace Data
    D--)T: Health Status

    Deep Dive: The AppHost Configuration

    The heart of Aspire is the AppHost.cs file. Here’s our project’s configuration:

    var builder = DistributedApplication.CreateBuilder(args);
    // Database initialization scripts
    var scriptsPath = Path.Combine(builder.AppHostDirectory, "..", "..", "scripts");
    // PostgreSQL with pgAdmin and data persistence
    var postgres = builder.AddPostgres("postgres")
    .WithDataVolume("postgres-data")
    .WithPgAdmin()
    .WithBindMount(scriptsPath, "/docker-entrypoint-initdb.d");
    var contactsDb = postgres.AddDatabase("contactsdb", "contacts");
    // Backend API - automatic database reference
    var api = builder.AddProject<Projects.Contact_Api>("contact-api")
    .WithReference(contactsDb)
    .WaitFor(contactsDb)
    .WithEnvironment(context =>
    {
    context.EnvironmentVariables["AppSettings__ConnectionStrings__DefaultConnection"] =
    contactsDb.Resource.ConnectionStringExpression;
    });
    // Angular Frontend - automatic API reference
    var frontend = builder.AddNpmApp("frontend", "../../frontend", "serve")
    .WithReference(api)
    .WaitFor(api)
    .WithHttpEndpoint(targetPort: 4200, env: "PORT")
    .WithExternalHttpEndpoints()
    .PublishAsDockerFile();
    builder.Build().Run();

    What This Code Achieves

    That handful of lines replaces a pile of manual wiring:

    FeatureTraditional ApproachWith Aspire
    Database ConnectionManual connection string in appsettings.jsonWithReference(contactsDb) - automatic injection
    Service URLsHardcoded http://localhost:5217service://contact-api - automatic discovery
    Container StartupManual docker-compose upWaitFor() - automatic dependency ordering
    Health ChecksCustom implementationBuilt-in with /health and /alive endpoints

    Service Discovery: How It Works

    One of Aspire’s most useful features is automatic service discovery. No more hardcoded URLs.

    How Service Discovery Works

    When you call WithReference(api) in the AppHost, Aspire:

    1. Registers the service in its internal service registry
    2. Generates environment variables with service endpoints
    3. Configures HttpClient to resolve service names automatically
    4. Handles port changes transparently
    // In your Angular proxy or API calls, instead of:
    // "http://localhost:5217/api/contacts"
    // You use:
    // "service://contact-api/api/contacts"
    // Aspire resolves this at runtime!

    OpenTelemetry: Built-in Observability

    Aspire ships OpenTelemetry support ready to go through the ServiceDefaults project:

    public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder)
    where TBuilder : IHostApplicationBuilder
    {
    // Structured Logging
    builder.Logging.AddOpenTelemetry(logging =>
    {
    logging.IncludeFormattedMessage = true;
    logging.IncludeScopes = true;
    });
    // Metrics and Tracing
    builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics =>
    {
    metrics.AddAspNetCoreInstrumentation()
    .AddHttpClientInstrumentation()
    .AddRuntimeInstrumentation();
    })
    .WithTracing(tracing =>
    {
    tracing.AddSource(builder.Environment.ApplicationName)
    .AddAspNetCoreInstrumentation()
    .AddHttpClientInstrumentation();
    });
    return builder;
    }

    That one ConfigureOpenTelemetry call gives you structured logs, distributed traces, and runtime metrics across every service, with no per-service setup.


    The Aspire Dashboard

    When you run your Aspire application, the dashboard launches automatically. It gives you a resources view (service status, endpoints, environment variables, start/stop controls), live console logs filtered by service, a traces view with cross-service waterfalls, and a metrics view.

    Dashboard Access

    After running dotnet run --project aspire/AppHost, the dashboard is available at:

    EnvironmentURL
    Local Developmenthttps://localhost:17178
    Aspire Dashboard Resources tab showing all 6 resources running: pgadmin, postgres with contactsdb, Angular frontend, postgres-password parameter, and contact-api, all green
    Aspire Dashboard, Resources tab for the Clean Architecture project. One command starts everything: pgAdmin, PostgreSQL with the contactsdb database, the Angular frontend (npm run serve), and the .NET API, all with live endpoints and health status.

    Why Use Aspire for Microservices

    The gain lands in three places: developer experience (single-command startup, hot reload, easier debugging), operations (built-in monitoring, health checks, dependency ordering), and portability (the same topology runs locally and in the cloud).

    Comparison: Before and After Aspire

    AspectWithout AspireWith Aspire
    Setup TimeHours/DaysMinutes
    Configuration FilesMultiple (docker-compose, .env, etc.)Single AppHost.cs
    Service DiscoveryManual DNS/Environment varsAutomatic
    ObservabilitySeparate tool integrationBuilt-in
    Health ChecksCustom implementationAutomatic
    Local DevelopmentComplex multi-terminal setupSingle F5/dotnet run
    DebuggingDifficult across servicesNative IDE support

    Getting Started with .NET Aspire

    Prerequisites

    RequirementVersionDownload
    .NET SDK10.0+Download2
    Node.js22 LTSDownload3
    Docker DesktopLatestDownload4
    Angular Contact Portal dashboard showing Dashboard, Contacts and Admin navigation, with seeded contacts and technology cards for PostgreSQL, .NET 10, Angular 21, and .NET Aspire
    The Angular frontend, running via npm run serve, discovered automatically by the .NET API through Aspire service references. Seeded contacts load from PostgreSQL on first run.

    Quick Start Commands

    Terminal window
    # Clone the repository
    git clone https://github.com/nitin27may/clean-architecture-docker-dotnet-angular.git
    cd clean-architecture-docker-dotnet-angular
    # Install frontend dependencies
    cd frontend && npm install && cd ..
    # Run with Aspire
    dotnet run --project aspire/AppHost

    Creating a New Aspire Project from Scratch

    Terminal window
    # Install Aspire workload
    dotnet workload install aspire
    # Create new Aspire starter app
    dotnet new aspire-starter -n MyAspireApp
    # Navigate and run
    cd MyAspireApp
    dotnet run --project MyAspireApp.AppHost

    Using the Aspire CLI

    The Aspire CLI provides additional capabilities:

    Terminal window
    # Install Aspire CLI
    dotnet tool install -g aspire.cli
    # Create new project
    aspire new starter --name MyApp
    # Run project
    aspire run
    # Add integrations
    aspire add postgres
    aspire add redis
    aspire add rabbitmq

    Aspire Integrations Ecosystem

    Aspire ships ready-to-use integrations for the usual suspects: PostgreSQL, SQL Server, MySQL, MongoDB, and Cosmos DB for data; Redis, Garnet, and Valkey for caching; RabbitMQ, Kafka, Azure Service Bus, and NATS for messaging; Azure Storage and AWS S3 for blobs. Each is a NuGet package with a standard registration.

    Adding Integrations

    // PostgreSQL with pgAdmin
    var postgres = builder.AddPostgres("postgres")
    .WithPgAdmin()
    .WithDataVolume();
    // Redis for caching
    var redis = builder.AddRedis("cache");
    // RabbitMQ for messaging
    var rabbitmq = builder.AddRabbitMQ("messaging");
    // Reference in your API
    builder.AddProject<Projects.MyApi>("api")
    .WithReference(postgres)
    .WithReference(redis)
    .WithReference(rabbitmq);

    From Development to Production

    The same AppHost.cs topology that runs locally maps to production targets: aspire deploy can push it to Azure Container Apps, Kubernetes, AWS ECS, or Docker Compose, keeping the same service graph with production-sized resources.

    Deployment Example

    ComponentLocal (Aspire)AzureAWS
    Frontendnpm serveAzure Container AppsECS
    APIdotnet runAzure Container AppsLambda
    DatabaseDocker containerAzure PostgreSQLRDS

    Best Practices

    1. Project Structure

    ├── aspire/
    │ ├── AppHost/ # Orchestration
    │ │ └── AppHost.cs # Service definitions
    │ └── ServiceDefaults/ # Shared configurations
    │ └── Extensions.cs # OpenTelemetry, health checks
    ├── backend/
    │ └── Contact.Api/ # Your API project
    ├── frontend/
    │ └── src/ # Angular application
    └── scripts/
    └── seed-data.sql # Database initialization

    2. Always Use ServiceDefaults

    // In your API's Program.cs
    var builder = WebApplication.CreateBuilder(args);
    builder.AddServiceDefaults(); // Add this line!
    var app = builder.Build();
    app.MapDefaultEndpoints(); // Health check endpoints
    app.Run();

    3. Use WaitFor for Dependencies

    var api = builder.AddProject<Projects.Api>("api")
    .WithReference(database)
    .WaitFor(database) // Ensures DB is ready
    .WithReference(cache)
    .WaitFor(cache); // Ensures cache is ready

    Troubleshooting Common Issues

    IssueSolution
    Port already in useStop other services or change port in AppHost
    Docker not runningStart Docker Desktop first
    Node modules missingRun npm install in frontend folder
    Database not initializingCheck scripts folder path in WithBindMount
    Services not discoveringCheck that WithReference is configured

    Try it on your own stack

    If you’re running .NET plus Angular plus PostgreSQL, clone the repo1 and run dotnet run --project aspire/AppHost. You get the frontend, API, database, and pgAdmin under one command, with service discovery and OpenTelemetry already wired. It’s a low-risk change: the application code doesn’t move, and you can back it out by deleting two projects. Give the dashboard’s trace view ten minutes and you’ll see why it stays.


    Resources

    Official Documentation

    This Project

    • GitHub Repository1

    Community


    Next Steps

    Related articles:


    References

    1. Clean Architecture with .NET Aspire — github.com 2 3

    2. Download — dotnet.microsoft.com

    3. Download — nodejs.org

    4. Download — docker.com

    5. Aspire Dashboard Documentation — aspire.dev

    Comments

    Comments are GitHub discussions. Sign in with GitHub to post; reactions need no account.