Skip to main content
  1. Blog/

Adding .NET Aspire to an Existing Clean Architecture Project

·10 mins
Nitin Kumar Singh
Author
Nitin Kumar Singh
I build enterprise AI solutions and cloud-native systems. I write about architecture patterns, AI agents, Azure, and modern development practices — with full source code.
Table of Contents

The infrastructure overhead in microservices projects is real. Setting up databases, wiring service discovery, getting distributed tracing working, managing connection strings across environments—this work hits before you’ve written a line of business logic. On more than one project, I’ve watched the first sprint disappear into Docker Compose files and appsettings drift.

.NET Aspire addresses this directly. It’s a cloud-ready stack that lets you define your entire application topology in C# code.

In this article, I’ll show how to add .NET Aspire to an existing Clean Architecture project with an Angular frontend, .NET backend API, and PostgreSQL database—no rewrite required.

Source Code: Clean Architecture with .NET Aspire

Note: This project demonstrates adding .NET Aspire to an existing application — not starting from scratch. The migration path is deliberately minimal: two new projects, one line in Program.cs, and a project reference.


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:

    dotnet workload install aspire
  2. Create the AppHost project:

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

    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
#

Here’s a diagram of the pain points Aspire is designed to replace:

flowchart TB subgraph Traditional["Traditional Microservices Development"] A[Manual Service Discovery] --> B[Hardcoded Connection Strings] B --> C[Complex Docker Compose Files] C --> D[Separate Logging Solutions] D --> E[Manual Health Checks] E --> F[Environment Variable Management] F --> G[Infrastructure Setup Time] end subgraph Problems["Common Problems"] H["Days spent on infrastructure"] I["Different configs per environment"] J["No unified observability"] K["Hard to debug distributed calls"] L["Service dependency chaos"] end Traditional --> Problems style Traditional fill:#ffcccc,stroke:#cc0000 style Problems fill:#ffe6cc,stroke:#cc6600

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 cloud-ready stack for building observable, production-ready, distributed applications. At its core is the AppHost - a code-first orchestrator that defines your application’s services, resources, and connections.

mindmap root((.NET Aspire)) AppHost Orchestration Service Discovery Connection Management Resource Lifecycle Environment Configuration Integrations Databases PostgreSQL SQL Server MongoDB Caching Redis Garnet Messaging RabbitMQ Kafka Azure Service Bus Observability OpenTelemetry Distributed Tracing Metrics Collection Structured Logging Dashboard Real-time Monitoring Log Aggregation Trace Visualization Health Checks

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:

flowchart TB subgraph Browser["Browser"] User[User] end subgraph Aspire[".NET Aspire Orchestration"] subgraph Dashboard["Aspire Dashboard"] Logs[Logs] Traces[Traces] Metrics[Metrics] Resources[Resources] end subgraph Services["Orchestrated Services"] Frontend["Angular Frontend
(npm serve)"] API[".NET API
Clean Architecture
(contact-api)"] DB["PostgreSQL
Database
(contactsdb)"] PgAdmin["pgAdmin
DB Management"] end end User -->|"HTTP Requests"| Frontend Frontend -->|"API Calls
(Service Discovery)"| API API -->|"Data Access
(Auto Connection String)"| DB PgAdmin -->|"Management"| DB Services -.->|"Telemetry Data"| Dashboard style Aspire fill:#e6f3ff,stroke:#0066cc style Dashboard fill:#fff2cc,stroke:#cc9900 style Services fill:#e6ffe6,stroke:#009900

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
#

flowchart LR subgraph AppHost["AppHost.cs Features"] direction TB subgraph Auto["Automatic Features"] SD[Service Discovery] CS[Connection Strings] HM[Health Monitoring] DM[Container Management] end subgraph Config["Zero Config Needed"] URL[No Hardcoded URLs] ENV[No .env Files] YAML[No docker-compose.yml] DNS[No DNS Setup] end end AppHost --> |"Eliminates"| Manual["Manual Infrastructure Work"] style AppHost fill:#e6ffe6,stroke:#009900 style Auto fill:#fff2cc,stroke:#cc9900 style Config fill:#e6f3ff,stroke:#0066cc
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.

flowchart TB subgraph Traditional["Traditional Approach"] T1["Frontend: http://localhost:5217/api"] T2["API: Server=localhost;Port=5432;..."] T3["Different configs per environment"] T4["Easy to break when ports change"] end subgraph Aspire["Aspire Approach"] A1["Frontend: service://contact-api"] A2["API: Injected automatically"] A3["Same code everywhere"] A4["Resilient to infrastructure changes"] end Traditional -->|"Replaced by"| Aspire style Traditional fill:#ffcccc,stroke:#cc0000 style Aspire fill:#ccffcc,stroke:#009900

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

What’s Included
#

flowchart TB subgraph Observability["Complete Observability Stack"] subgraph Logging["Structured Logs"] L1[Request/Response Logs] L2[Exception Details] L3[Custom Log Scopes] end subgraph Tracing["Distributed Tracing"] TR1[Request Timelines] TR2[Service Dependencies] TR3[Performance Bottlenecks] TR4[Error Propagation] end subgraph Metrics["Metrics"] M1[Request Rates] M2[Response Times] M3[Error Rates] M4[Runtime Stats] end end Dashboard["Aspire Dashboard"] --> Observability style Dashboard fill:#fff2cc,stroke:#cc9900 style Observability fill:#e6f3ff,stroke:#0066cc

The Aspire Dashboard
#

When you run your Aspire application, the dashboard launches automatically:

flowchart TB subgraph Dashboard["Aspire Dashboard Features"] subgraph Resources["Resources View"] R1["Service Status"] R2["Endpoint URLs"] R3["Environment Variables"] R4["Start/Stop Controls"] end subgraph Console["Console Logs"] C1["Real-time Log Streaming"] C2["Filter by Service"] C3["Search Within Logs"] C4["Color-coded Levels"] end subgraph Traces["Traces View"] T1["Request Timelines"] T2["Waterfall Diagrams"] T3["Cross-service Calls"] T4["Latency Analysis"] end subgraph Metrics["Metrics View"] MT1["HTTP Request Metrics"] MT2["Response Time Histograms"] MT3["Resource Utilization"] MT4["Custom Instruments"] end end style Dashboard fill:#e6ffe6,stroke:#009900

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
#

flowchart TB subgraph Benefits["Key Benefits"] direction LR subgraph Dev["Developer Experience"] D1["Single command startup"] D2["Hot reload support"] D3["Easy debugging"] end subgraph Ops["Operations"] O1["Built-in monitoring"] O2["Health checks"] O3["Dependency management"] end subgraph Scale["Scalability"] S1["Cloud-ready design"] S2["Container support"] S3["Same code, any environment"] end end Aspire[".NET Aspire"] --> Benefits style Aspire fill:#4a90d9,stroke:#2c5282,color:#ffffff style Benefits fill:#e6f3ff,stroke:#0066cc

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+Download
Node.js22 LTSDownload
Docker DesktopLatestDownload

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
#

# 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
#

# 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:

# 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 provides ready-to-use integrations for popular services:

flowchart TB subgraph Integrations["Aspire Integrations"] subgraph Databases["Databases"] DB1["PostgreSQL"] DB2["SQL Server"] DB3["MySQL"] DB4["MongoDB"] DB5["CosmosDB"] end subgraph Caching["Caching"] C1["Redis"] C2["Garnet"] C3["Valkey"] end subgraph Messaging["Messaging"] M1["RabbitMQ"] M2["Kafka"] M3["Azure Service Bus"] M4["NATS"] end subgraph Cloud["Cloud Services"] CL1["Azure Storage"] CL2["AWS S3"] end end style Integrations fill:#e6f3ff,stroke:#0066cc

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 topology that runs locally maps directly to production deployment targets:

flowchart LR subgraph Dev["Development"] D1["AppHost.cs
defines topology"] end subgraph Deploy["Deployment Options"] DP1["Azure Container Apps"] DP2["Kubernetes"] DP3["AWS ECS"] DP4["Docker Compose"] end subgraph Prod["Production"] P1["Same service topology"] P2["Production resources"] P3["Scaled instances"] end Dev -->|"aspire deploy"| Deploy Deploy --> Prod style Dev fill:#fff2cc,stroke:#cc9900 style Deploy fill:#e6f3ff,stroke:#0066cc style Prod fill:#ccffcc,stroke:#009900

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 discoveringEnsure WithReference is configured

Conclusion
#

The steps in this post are exactly what I followed to add Aspire to this existing Clean Architecture project. No rewrite, no restructuring of the application code—just two new projects (AppHost and ServiceDefaults), a one-line addition to Program.cs, and a reference added to the API project file.

The payoff is immediate: one command starts the Angular frontend, the .NET API, PostgreSQL, and pgAdmin. Service discovery is automatic. OpenTelemetry is wired. The dashboard shows you logs and traces across everything without any additional tooling.

If you’re running a similar stack—.NET API, Angular, PostgreSQL—clone the repo and run dotnet run --project aspire/AppHost. The migration path is low-risk and the observability improvements alone justify it.


Resources
#

Official Documentation
#

This Project
#

Community
#


Next Steps
#

Related articles:


Related

Building Production-Ready Microservices with .NET Aspire: A Complete E-Commerce Demo

·24 mins
If you’ve ever built a microservices architecture, you know the pain points all too well: spending hours setting up PostgreSQL locally, wrestling with Redis configurations, debugging why RabbitMQ won’t connect, managing connection strings across multiple services, and let’s not even talk about implementing distributed tracing manually.

Handling Authorization and Role-Based Access Control (RBAC)

·19 mins
Introduction # Role-Based Access Control (RBAC) is a critical component of secure application design that restricts access to resources based on user roles and permissions. This article explores how the Contact Management Application implements a flexible and maintainable RBAC system that covers both the backend API and frontend Angular application, integrating with JWT authentication to secure endpoints and UI elements while maintaining the separation of concerns that Clean Architecture demands.

Clean Architecture: Introduction to the Project Structure

·11 mins
Overview # Clean Architecture is a powerful software design pattern that promotes a clear separation of concerns, making your application’s core business logic independent of external dependencies like databases, user interfaces, or frameworks. By following this architecture, systems become maintainable, testable, and adaptable, preparing them for both current demands and future growth.